diff --git a/template/.claude/skills/markdown/SKILL.md b/template/.claude/skills/markdown/SKILL.md new file mode 100644 index 0000000..1dbb651 --- /dev/null +++ b/template/.claude/skills/markdown/SKILL.md @@ -0,0 +1,179 @@ +--- +name: markdown +description: >- + Expert guidance for writing, editing, managing, and handling Markdown (.md) files + to a professional standard. Use this skill whenever the user wants to: create or + write a new Markdown file, edit or improve an existing .md file, review Markdown + for style/consistency, convert content into well-structured Markdown, build README + files, technical docs, changelogs, wikis, or any documentation in .md format. + Also trigger for requests involving front matter/metadata, table formatting, code + blocks, link hygiene, accessibility in Markdown, GitHub-flavored Markdown (GFM), + or best practices for Markdown documentation systems. If the user mentions .md, + README, CHANGELOG, docs, or wiki, use this skill. +--- + +# Markdown Skill + +A comprehensive skill for producing, editing, and managing Markdown files that are +readable, portable, accessible, and maintainable over time. + +## Quick reference: where to go deeper + +| Topic | Reference file | +|------------------------------------|-------------------------------------------------------------------------------| +| Document structure and front matter | [references/document-structure.md](references/document-structure.md) | +| Formatting syntax (emphasis, lists) | [references/formatting-syntax.md](references/formatting-syntax.md) | +| Code blocks and links | [references/code-and-links.md](references/code-and-links.md) | +| Tables, images, and HTML | [references/tables-images-html.md](references/tables-images-html.md) | +| Extended syntax (GFM, callouts) | [references/extended-syntax.md](references/extended-syntax.md) | +| File management and doc systems | [references/file-management.md](references/file-management.md) | +| Anti-patterns and cheat sheet | [references/anti-patterns-cheatsheet.md](references/anti-patterns-cheatsheet.md) | + +Read the relevant reference file before working on a specific area. For a new README, +skim `document-structure.md` first. For table formatting or GFM features, check the +corresponding reference. + +--- + +## Primary references + +This skill is grounded in the **Google Markdown Style Guide** and extended with best +practices from CommonMark, Markdown Guide, and the GitHub Flavored Markdown Spec. + +--- + +## Core philosophy + +Three goals govern every Markdown file: + +1. **Source text is readable and portable** — raw `.md` source should be legible as + plain text without rendering. +2. **The corpus is maintainable over time** — consistent conventions let any author + pick up where another left off. +3. **Syntax is simple and memorable** — favour standard Markdown over HTML hacks or + exotic extensions. + +--- + +## Essential rules + +### Document structure + +Every well-formed Markdown document follows this skeleton: + +```markdown +# Document Title + +Short introduction (1-3 sentences). Explain what this document is and who it is for. + +## First Topic + +Content. + +## Second Topic + +Content. + +## See Also + +- [Link to related resource](https://example.com) +``` + +- Exactly one `# H1` per document — the page title. +- 1-3 sentence introduction before any sections. +- Start subsequent headings at `## H2`; never skip levels. +- Sentence-case headings (capitalise first word and proper nouns only). + +### Headings + +Use ATX-style (`#`, `##`, `###`). Never use Setext (underline) style. Add blank lines +before and after every heading. Give headings unique, descriptive names to avoid anchor +collisions. + +### Line length + +80-character limit for body text. Exceptions: links, table cells, headings, and code +blocks. Use a blank line to separate paragraphs — do not rely on trailing whitespace. + +### Emphasis + +- `**bold**` for critical terms, UI labels, and warnings. +- `*italic*` for introducing new terms and light emphasis. +- Never bold or italicise entire paragraphs — overuse kills emphasis. +- Use `*asterisks*` not `_underscores_` for mid-word emphasis. + +### Lists + +- Use `-` consistently for unordered lists. +- Use explicit numbers (`1.`, `2.`, `3.`) for short stable lists. +- Use lazy numbering (`1.`, `1.`, `1.`) for long or frequently changing lists. +- 4-space indent for wrapped text and nested lists. + +### Code + +Always use fenced blocks with a language tag. Never use 4-space indented blocks: + +````markdown +```python +def greet(name: str) -> str: + return f"Hello, {name}!" +``` +```` + +Use backticks for inline code: commands, function names, file names, paths. + +### Links + +Write the sentence naturally, then wrap the most descriptive phrase as the link text. +Never use "click here" or "here" as link text. Use relative paths for internal links: + +```markdown +See the [contributing guide](docs/CONTRIBUTING.md) for details. +``` + +### Tables + +Use tables only when data is genuinely two-dimensional. Keep cells short. Use +reference links for URLs in table cells. If a table has only 1-2 columns, prefer a +list instead. + +### Images + +Always write descriptive alt text. Store images in a dedicated `images/` or `assets/` +directory. Prefer SVG for diagrams and PNG for screenshots. + +### HTML + +Strongly prefer plain Markdown over HTML. Acceptable uses: `
`, `
`, +``/`` — only as a last resort. + +### Accessibility + +- Descriptive alt text on every meaningful image. +- Meaningful link text (not "here" or bare URLs). +- Logical heading hierarchy for assistive navigation. +- Semantic emphasis (`**important**`) rather than formatting for decoration. + +--- + +## Common anti-patterns + +| Anti-pattern | Fix | +|----------------------------------|----------------------------------------| +| Multiple H1s | Keep exactly one — the page title | +| Skipping heading levels | Follow strict hierarchy | +| `[click here](url)` | Write descriptive link text | +| 4-space indented code blocks | Use fenced blocks with language tag | +| Mixing `*` and `-` for bullets | Pick one style per file | +| HTML ``, `` tags | Use `**` and `*` | +| Tables for simple lists | Use a list instead | +| Empty alt text on images | Always write descriptive alt text | + +--- + +## See Also + +- [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html) +- [CommonMark Specification](https://commonmark.org/) +- [Markdown Guide](https://www.markdownguide.org/) +- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/) diff --git a/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md b/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md new file mode 100644 index 0000000..31b7146 --- /dev/null +++ b/template/.claude/skills/markdown/references/anti-patterns-cheatsheet.md @@ -0,0 +1,323 @@ +# Anti-patterns and cheat sheet + +Covers: comprehensive list of common Markdown mistakes and how to fix them, +plus a complete syntax cheat sheet for quick reference. + +--- + +## Anti-Patterns — Common Mistakes and Fixes + +### Structure Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| Multiple `# H1` headings | Only one H1 per doc (page title rule) | Use H2+ for all sections after the title | +| Skipping heading levels (H2 → H4) | Breaks logical hierarchy; accessibility issue | Follow strict H1 → H2 → H3 → H4 order | +| No introduction before first section | Reader has no context | Add 1–3 sentence overview before first H2 | +| `[TOC]` at top before introduction | Screen readers miss the introduction | Place `[TOC]` after introduction, before H2 | +| Generic headings: `### Summary` under multiple H2s | Anchor link collisions | Use unique, descriptive names: `### Foo Summary` | +| Giant monolithic document (1000+ lines) | Hard to navigate and maintain | Split into focused sub-documents with an index | +| No `## See Also` at the end | Reader has nowhere to go for related info | Add outbound links in a final `## See Also` section | + +### Formatting Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| Bolding entire paragraphs | Emphasis loses meaning | Bold key phrases only | +| Mixing `*` and `-` and `+` for bullets | Inconsistent; confusing in source | Pick one style (`-`) and use it throughout | +| Using `_underscores_` for bold/italic | Inconsistent parser behaviour mid-word | Always use `*asterisks*` and `**asterisks**` | +| Setext-style headings (`===`, `---`) | Ambiguous (is `---` an H2 or a rule?); fragile | Use ATX `##` style only | +| Trailing whitespace on lines | Hidden; unreliable line-break behaviour | Use `\` for intentional breaks; none otherwise | +| 4-space indented code blocks | Cannot declare a language; ambiguous start/end | Use fenced ` ``` ` blocks with language tags | +| Code block with no language declared | No syntax highlighting; reader/editor must guess | Always declare: ` ```python `, ` ```bash `, etc. | +| HTML tags for bold (``) and italic (``) | Reduces portability | Use `**` and `*` | +| Excessive horizontal rules between every section | Fragments the doc visually | Use headings to separate sections; use `---` sparingly | + +### Link Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| `[click here](url)` | Uninformative to scanners and screen readers | Write descriptive text: `[Installation guide](url)` | +| `[here](url)` or `[this link](url)` | Same as above | Wrap the most descriptive phrase in the sentence | +| Bare URL as link text: `[https://example.com](https://example.com)` | Wastes space; no meaningful label | Use `[Example Site](https://example.com)` | +| `../../../relative/path.md` | Breaks on file restructuring | Use root-relative paths: `/docs/path.md` | +| Full GitHub URL for an internal doc link | Breaks when repo is renamed/moved | Use root-relative path | +| Long URL inline in a table cell | Makes table unreadable | Use reference-style links | +| Reference links defined at bottom of long file (first use in section 2, definition in last section) | Hard to find source | Define reference links just before the next heading after first use | + +### Image Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| Empty or missing alt text on meaningful images | Inaccessible to screen readers | Write descriptive alt text: `![Bar chart showing Q4 revenue up 23%](...)` | +| `![image](pic.png)`, `![screenshot](s.png)` | Meaningless alt text | Describe what the image shows and why it matters | +| Storing all images in root directory | Clutters root; hard to manage | Use `images/` or `assets/images/` subdirectory | +| Non-descriptive image filenames: `img1.png`, `screen.png` | Hard to find; no context | Use descriptive names: `auth-flow-diagram.svg` | +| JPEG for screenshots | Lossy compression blurs text | Use PNG for screenshots | +| PNG for photographs | Unnecessarily large file | Use JPEG for photographic images | +| Relying solely on an image to convey content | Breaks for screen readers | Always accompany diagrams with prose or captioned explanation | + +### Table Anti-Patterns + +The Google style guide names three specific table problems to diagnose before writing: + +| Named problem | Signs | Fix | +|---|---|---| +| **Poor distribution** | Columns don't differ across rows; cells are empty | The data doesn't need a table — use a list | +| **Unbalanced dimensions** | Very few rows vs. many columns (or vice versa) | Use a list with subheadings | +| **Rambling prose in cells** | Long sentences or paragraphs inside cells | Move prose out of the table | + +General table anti-patterns: + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| Table where a list would be clearer | Hard to maintain; verbose source | Use lists and subheadings | +| Mostly empty table cells | Data not appropriate for tabular form | Use lists or prose | +| Long URLs inline in table cells | Makes table source unreadable | Use reference links in table cells | +| Tables with a single column | Just use a list | Use `- item` list syntax | +| Prose paragraphs inside table cells | Markdown has no cell line-wrap | Move verbose content to a list or subsection | +| Inconsistent column alignment for numeric data | Hard to compare numbers | Right-align numeric columns with `---:` | + +### HTML Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| `
` for paragraph spacing | Not a paragraph break | Use a blank line | +| `

`, `

` instead of `##`, `###` | Breaks portability and TOC generation | Use ATX heading syntax | +| ``, `` instead of `**`, `*` | Reduces portability | Use Markdown emphasis | +| `
    `, `
      `, `
    1. ` for lists | Verbose; not Markdown | Use `-` and `1.` list syntax | +| CSS styling on individual elements | Renders in very few environments | Use structural Markdown only | +| HTML tables instead of pipe tables | Not necessary unless table is extremely complex | Use Markdown pipe-table syntax | + +### File Management Anti-Patterns + +| Anti-pattern | Why it's wrong | Fix | +|---|---|---| +| Spaces in filenames: `Getting Started.md` | Spaces break URLs | Use `getting-started.md` | +| Uppercase in non-standard files: `API_Reference.md` | URL path is case-sensitive; inconsistent | Use `api-reference.md` | +| All docs in the root directory | Clutters root; hard to navigate | Put docs in `docs/` subdirectory | +| No `README.md` in subdirectories | Reader has no entry point | Add an index `README.md` to each docs subdirectory | +| `CHANGELOG` without `.md` extension | May not render as Markdown on GitHub | Use `CHANGELOG.md` | +| Stale docs left indefinitely | Worse than no docs; misleads readers | Delete or archive stale content regularly | + +--- + +## Complete Syntax Cheat Sheet + +### Document Skeleton + +```markdown +--- +title: "Document Title" +date: "2024-01-15" +--- + +# Document Title + +Short introduction (1–3 sentences). + +[TOC] + +## Section One + +Content. + +## Section Two + +Content. + +## See Also + +- [Link one](https://example.com) +``` + +### Headings + +```markdown +# H1 — one per document +## H2 +### H3 +#### H4 +##### H5 +###### H6 +``` + +### Emphasis + +```markdown +**bold** Strong importance +*italic* Stress emphasis +***bold italic*** Both +~~strikethrough~~ Deprecated / error +`inline code` Code, commands, field names +``` + +### Lists + +```markdown +- Unordered item Use - consistently +- Another item + - Nested (2-space indent) + +1. Ordered item +2. Second item +1. Lazy numbering (also valid) + +- [x] Completed task GFM task list +- [ ] Pending task +``` + +### Code + +````markdown +`inline code` + +```python +# Fenced block — always declare language +def foo(): + pass +``` +```` + +### Links + +```markdown +[Link text](https://url.com) +[Link text](/root/relative/path.md) +[Link text](same-dir-file.md) +[Link text][ref-id] + +[ref-id]: https://long-url.com +``` + +### Images + +```markdown +![Alt text](path/to/image.png) +![Alt text](image.png "Optional tooltip") +[![Alt text](image.png)](https://link.com) ← clickable image +``` + +### Tables + +```markdown +| Left | Centre | Right | +|:-----|:------:|------:| +| Text | Text | 12.50 | +``` + +### Blockquotes and Callouts + +```markdown +> Standard blockquote + +> [!NOTE] GFM alert +> [!TIP] +> [!IMPORTANT] +> [!WARNING] +> [!CAUTION] +``` + +### Horizontal Rule + +```markdown +--- +``` + +### Footnotes (extended) + +```markdown +Body text.[^1] + +[^1]: Footnote content. +``` + +### Math (extended) + +```markdown +Inline: $E = mc^2$ + +Block: +$$ +\frac{d}{dx} f(x) = f'(x) +$$ +``` + +### Mermaid (extended) + +````markdown +```mermaid +flowchart LR + A --> B --> C +``` +```` + +### Escaping + +```markdown +\* \# \` \_ \[ \] \( \) \{ \} \. \! \| +``` + +### HTML (last resort only) + +```markdown +
      Hard line break +Ctrl Keyboard key +subscript Subscript fallback +superscript Superscript fallback +
      TitleBody
      Collapsible + Invisible author note +``` + +--- + +## Quick Review Checklist + +Use this when reviewing or finalising any Markdown file: + +**Structure** +- [ ] Exactly one `# H1` heading +- [ ] 1–3 sentence introduction before first section +- [ ] No skipped heading levels +- [ ] Heading names are unique and descriptive +- [ ] `[TOC]` placed after introduction (if used) +- [ ] `## See Also` at the end (if the doc has outbound links) + +**Formatting** +- [ ] Asterisks used for emphasis (not underscores) +- [ ] No bolded entire paragraphs +- [ ] Blank line before and after every heading +- [ ] Blank line between list and surrounding paragraphs +- [ ] Consistent bullet style (`-` throughout) +- [ ] No trailing whitespace + +**Code** +- [ ] All fenced code blocks have a language tag +- [ ] No 4-space indented code blocks +- [ ] Long commands use trailing `\` for line continuation + +**Links** +- [ ] All link text is descriptive (no "click here" / "here") +- [ ] Internal links use root-relative paths (not full URLs) +- [ ] No `../` cross-directory relative paths +- [ ] Reference links defined near their first use + +**Images** +- [ ] All meaningful images have descriptive alt text +- [ ] No `![image](...)` or `![screenshot](...)` alt text +- [ ] Images stored in `images/` or `assets/` directory + +**Tables** +- [ ] Tables only used where data is genuinely two-dimensional +- [ ] Table cells are short (reference links for URLs) +- [ ] Numeric columns right-aligned + +**HTML** +- [ ] No unnecessary HTML tags +- [ ] No HTML used for styling or layout + +**Files** +- [ ] File name is lowercase-hyphenated (except standard UPPERCASE files) +- [ ] No spaces in file names +- [ ] Standard files (README, CHANGELOG) in root directory +- [ ] Non-standard docs in `docs/` subdirectory diff --git a/template/.claude/skills/markdown/references/code-and-links.md b/template/.claude/skills/markdown/references/code-and-links.md new file mode 100644 index 0000000..0176919 --- /dev/null +++ b/template/.claude/skills/markdown/references/code-and-links.md @@ -0,0 +1,347 @@ +# Code and links + +Covers: inline code, fenced code blocks, language tags, escaping, all link types, +reference links, and automatic links. + +**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html) + +--- + +## Code + +### Inline Code + +Use single backticks for short code references, command names, file names, field +names, file extensions, paths, and environment variables: + +```markdown +Run `really_cool_script.sh` before proceeding. +Update your `README.md` to reflect the new API. +Pay attention to the `user_id` field in the response. +Set the `DATABASE_URL` environment variable. +Files with `.env` extension are excluded by default. +``` + +#### Using Inline Code as an Escape Mechanism + +Wrap anything that should **not** be parsed as Markdown in backticks — fake paths, +example URLs with query strings, template variables: + +```markdown +An example path would be: `path/to/your/config.yaml` +An example query: `https://api.example.com/search?q=$TERM&page=1` +A template variable: `{{ user.name }}` +``` + +#### Escaping Backticks Inside Inline Code + +Use double backticks to wrap inline code that itself contains a backtick: + +```markdown +`` Use `backticks` for inline code `` +``` + +--- + +### Fenced Code Blocks + +For any multi-line code, **always use fenced blocks** (triple backticks). + +**Never use 4-space indented code blocks:** +- Cannot declare a language for syntax highlighting +- Beginning and end of the block are ambiguous +- Harder to find in code search + +#### Always Declare the Language + +Declare the language immediately after the opening fence: + +````markdown +```python +def greet(name: str) -> str: + return f"Hello, {name}!" +``` +```` + +````markdown +```javascript +const greet = (name) => `Hello, ${name}!`; +``` +```` + +````markdown +```bash +git clone https://github.com/org/repo.git +cd repo && npm install +``` +```` + +````markdown +```json +{ + "name": "my-project", + "version": "1.0.0" +} +``` +```` + +````markdown +```yaml +server: + host: 0.0.0.0 + port: 8080 +``` +```` + +````markdown +```sql +SELECT user_id, email +FROM users +WHERE created_at > '2024-01-01' +ORDER BY created_at DESC; +``` +```` + +````markdown +```diff +- removed line ++ added line + unchanged line +``` +```` + +Specifying no language disables syntax highlighting — only do this for plain-text +output or when no language applies: + +````markdown +``` +Plain text output or logs with no language. +``` +```` + +#### Escape Long Command-Line Calls + +Use a trailing backslash to break long shell commands across lines so they can be +copied and pasted directly into a terminal: + +````markdown +```shell +bazel run :target -- \ + --flag \ + --foo=longlonglonglonglongvalue \ + --bar=anotherlonglonglonglonglongvalue +``` +```` + +#### Nesting Code Blocks Inside Lists + +Indent the fenced block by the same amount as the list item's text to avoid +breaking the list: + +```markdown +1. Clone the repo. + + ```bash + git clone https://github.com/org/repo.git + ``` + +2. Install dependencies. + + ```bash + npm install + ``` + +3. Start the server. + + ```bash + npm run dev + ``` +``` + +You can also nest a code block using **4 additional spaces** from the list +indentation (no fences required, but no language tag either): + +```markdown +* Bullet point. + + int foo; ← indented 4 spaces beyond the bullet text + +* Next bullet. +``` + +Prefer the fenced approach (explicit language tag, unambiguous boundaries). +Use the 4-space approach only when the platform does not support fenced blocks. + +--- + +## Links + +> **General rule:** Long links make source Markdown difficult to read and break +> 80-character line wrapping. **Wherever possible, shorten your links.** + +### Inline Links + +Write the sentence naturally first, then wrap the most descriptive phrase as the +link text. Link text should describe the destination, not the act of clicking: + +```markdown +# ✅ Good — descriptive link text +See the [Markdown style guide](https://google.github.io/styleguide/docguide/style.html) for details. +Read the [contributing guide](CONTRIBUTING.md) before opening a PR. + +# ❌ Bad — uninformative link text +See the style guide [here](https://google.github.io/styleguide/docguide/style.html). +For more info, click [this link](https://example.com). +Check out [https://example.com/foo/bar](https://example.com/foo/bar). +``` + +Never use "click here", "here", "link", "this", or bare URLs as link text. + +### Links Within the Same Repository + +Use root-relative paths (not full URLs) for internal Markdown links: + +```markdown +# ✅ Root-relative path +[Contributing Guide](/docs/CONTRIBUTING.md) +[API Reference](/docs/api/README.md) + +# ✅ Same-directory relative link +[See also](other-page-in-same-dir.md) + +# ❌ Full URL for an internal doc +[Contributing Guide](https://github.com/org/repo/blob/main/docs/CONTRIBUTING.md) +``` + +Avoid `../` relative paths across directory levels — they break when files are +moved or the doc tree is restructured: + +```markdown +# ❌ Fragile cross-directory relative path +[Config Reference](../../bad/path/to/config.md) +``` + +### Link to a Heading Anchor + +```markdown +[Jump to Installation](#installation) +[See the Configuration section](#configuration-options) +``` + +Anchors are auto-generated: lowercase, spaces → hyphens, punctuation removed. + +### Reference-Style Links + +Use reference links when: +- The URL is long enough to break the 80-character line limit. +- The same URL is used more than once in the document. +- The link appears inside a table cell. + +```markdown +See the [Markdown style guide][google-style] for details. +Also read the [CommonMark spec][commonmark]. + +[google-style]: https://google.github.io/styleguide/docguide/style.html +[commonmark]: https://commonmark.org/ +``` + +#### When NOT to Use Reference Links + +Do not use reference links when the URL is short enough that inlining it does not +disrupt the flow of the text. Adding reference syntax for a short URL adds noise +without benefit: + +```markdown +# ❌ Unnecessary — URL is short, inline is cleaner +The [style guide][style_guide] says not to over-use reference links. + +[style_guide]: https://google.com/markdown-style + +# ✅ Just inline it +The [style guide](https://google.com/markdown-style) says not to over-use reference links. +``` + +Only use reference links when the URL is genuinely long enough to hurt readability +if inlined, when the URL is reused, or when inside a table cell. + +#### Where to Define Reference Links + +Define reference link definitions **just before the next heading** after their first +use (treat them like footnotes anchored to the current section). + +> **Note:** If your editor has its own opinion about where reference links should go, +> don't fight it — **the tools always win.** The goal is consistency; let your +> tooling enforce placement automatically. + +```markdown +## Authentication + +See the [OAuth 2.0 guide][oauth] for implementation details. +The [JWT specification][jwt] covers token structure. + +[oauth]: https://oauth.net/2/ +[jwt]: https://datatracker.ietf.org/doc/html/rfc7519 + +## Authorization + +... +``` + +**Exception:** Reference links used in **multiple sections** go at the end of the file +to avoid dangling links when sections are moved or restructured. + +#### Reference Links in Tables + +Always use reference links inside table cells — inline URLs make tables unreadable: + +```markdown +# ✅ Readable table with reference links +| Site | Description | +|---------|-------------------------| +| [MDN] | Web platform reference | +| [devdocs] | Unified API reference | + +[MDN]: https://developer.mozilla.org +[devdocs]: https://devdocs.io + +# ❌ Unreadable table with inline links +| Site | Description | +|-----------------------------------------|-------------------------| +| [MDN](https://developer.mozilla.org) | Web platform reference | +``` + +#### Reducing Duplication with Reference Links + +When the same URL appears three or more times in a document, define it once as a +reference link and use the reference throughout. + +### Automatic Links + +Most modern Markdown processors auto-link bare URLs. Use angle brackets to force +a URL to render as a clickable link when auto-linking is not enabled: + +```markdown + + +``` + +To **prevent** a URL from auto-linking, wrap it in backticks: + +```markdown +`https://www.example.com/search?q=$TERM` +``` + +--- + +## Link Quality Rules (Summary) + +| Rule | Rationale | +|---|---| +| Descriptive link text | Screen readers and scanners read link text in isolation | +| Root-relative paths for internal links | Portable; survives file moves | +| No bare-URL link text | Wastes space; conveys no information | +| No `../` cross-directory paths | Breaks on restructuring | +| Reference links for long URLs | Keeps body text within 80-char limit | +| Reference links in tables | Keeps table cells short and readable | +| Define reference links near first use | Easy to find; reduces "footnote overload" | +| Audit links periodically | Broken links erode reader trust | diff --git a/template/.claude/skills/markdown/references/document-structure.md b/template/.claude/skills/markdown/references/document-structure.md new file mode 100644 index 0000000..cd94d83 --- /dev/null +++ b/template/.claude/skills/markdown/references/document-structure.md @@ -0,0 +1,274 @@ +# Document structure + +Covers: document layout, front matter / metadata, headings, table of contents, +paragraphs, spacing, and capitalization. + +**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html) + +--- + +## The Better/Best Rule (Documentation Review Culture) + +The standards for a documentation review are different from code reviews. The +Google Markdown Style Guide defines the "Better/Best Rule" — fast iteration +produces better long-term quality than perfection on every change. + +> **"A small improvement shipped is better than a perfect doc never merged."** + +### As a reviewer + +1. When reasonable, **LGTM immediately** and trust that comments will be fixed appropriately. +2. **Prefer to suggest an alternative** rather than leaving a vague comment. +3. For substantial changes, **start your own follow-up CL** instead of blocking the author. Especially avoid comments of the form "You should *also*…". +4. On rare occasions, **hold up submission only if the CL actually makes the docs worse**. It's okay to ask the author to revert. + +### As an author + +1. **Avoid wasting cycles with trivial argument.** Capitulate early and move on. +2. **Cite the Better/Best Rule** as often as needed when reviewers ask for more than is necessary. + +Fast iteration is your friend. To get long-term improvement, **authors must stay +productive** when making short-term improvements. Set lower standards for each +change so that more such changes can happen. + +--- + +## Standard Document Layout + +Every well-formed Markdown document follows this skeleton: + +```markdown +# Document Title + +Short introduction (1–3 sentences). What is this? Who is it for? + +[TOC] + +## First Major Section + +Content. + +## Second Major Section + +Content. + +## See Also + +- [Related resource](https://example.com) +- [Another link](https://example.com) +``` + +### Layout Rules + +| Element | Rule | +|---|---| +| `# H1` | Exactly **one** per document — the page title | +| Introduction | 1–3 sentences high-level overview, before any TOC or section | +| `[TOC]` | After introduction, before first H2 (accessibility: screen readers read it in DOM order) | +| Subsequent headings | Start at H2; never skip levels (H2 → H4 is invalid) | +| `## See Also` | Last section; collect miscellaneous outbound links here | +| Author field | Optional, below the title — revision history usually suffices | + +> **TOC placement matters for accessibility.** The `[TOC]` directive inserts HTML into +> the DOM exactly where it appears. Placing it at the bottom means screen readers won't +> reach it until the end of the document. Always place it after the introduction. + +--- + +## Front Matter (YAML Metadata) + +When the target platform supports it (Jekyll, Hugo, Docusaurus, GitHub Pages, MkDocs): + +```markdown +--- +title: "Introduction to Python" +author: "Jane Smith" +date: "2024-08-03" +description: "An introductory guide covering basic concepts and syntax." +tags: ["python", "programming", "beginner"] +category: "Programming Tutorials" +version: "1.0.0" +last_updated: "2024-08-03" +--- +``` + +### Front Matter Rules + +- Always place front matter at the **very top** of the file, before any Markdown content. +- Use triple dashes (`---`) as opening and closing delimiters. +- Keep keys **lowercase** and **snake_cased**. +- Include `version` and `last_updated` for versioned API or library docs. +- The `description` field feeds SEO meta tags on documentation sites. +- `tags` and `category` improve discoverability in doc search systems. + +--- + +## Headings + +### Use ATX-Style Only + +```markdown +# H1 — Document Title +## H2 — Major Section +### H3 — Subsection +#### H4 — Sub-subsection +##### H5 +###### H6 +``` + +**Never** use Setext (underline) style — it is ambiguous, fragile, and hard to maintain: + +```markdown +Heading — DO NOT USE +-------------------- +``` + +An editor cannot tell at a glance whether `---` means H2 or a horizontal rule. + +### Heading Rules (Google Style Guide) + +1. **One H1 per document.** It becomes the page ``. +2. **Never skip levels.** H2 → H3 is valid. H2 → H4 is not. +3. **Unique, fully descriptive names.** Anchor links are auto-generated from heading text. + Generic repeated names (`### Summary` under multiple H2s) produce anchor collisions: + + ```markdown + # ❌ BAD — duplicate anchors break navigation + ## Foo + ### Summary + ## Bar + ### Summary + + # ✅ GOOD — unique, descriptive + ## Foo + ### Foo Summary + ### Foo Example + ## Bar + ### Bar Summary + ### Bar Example + ``` + +4. **Add blank lines** before and after every heading — required for consistent parsing. + + ```markdown + ...text before. + + ## Heading 2 + + Text after... + ``` + +5. **Sentence case** headings unless the target style guide mandates title case. + - Capitalise the first word and proper nouns only. + - Exception: product/tool names must preserve their exact capitalisation + (`GitHub`, `macOS`, `Node.js`, `TypeScript`). + +--- + +## Table of Contents + +### When to Include a TOC + +Include a TOC whenever the document has content "below the fold" on a typical laptop +screen (i.e., more than ~2 screens of content). + +### Manual TOC (universal Markdown) + +```markdown +## Contents + +1. [Installation](#installation) +2. [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Config File](#config-file) +3. [Usage](#usage) +4. [API Reference](#api-reference) +``` + +Anchor links are generated by: +- Converting heading text to lowercase +- Replacing spaces with hyphens +- Removing all punctuation except hyphens + +```markdown +## My Section Title → #my-section-title +## API: v2.0 → #api-v20 +``` + +### Directive TOC (platform-specific) + +```markdown +[TOC] <!-- Gitiles, MkDocs --> +[[toc]] <!-- VuePress --> +{:toc} <!-- Kramdown / Jekyll --> +``` + +--- + +## Paragraphs and Line Length + +### 80-Character Line Limit + +Body text follows an 80-character line limit (matches code conventions; improves +diff readability in version control). + +**Exceptions** that may exceed 80 characters: +- URLs and hyperlinks +- Table cells +- Headings +- Code blocks + +### Paragraph Rules + +- Separate paragraphs with **a single blank line**. +- Do not use trailing spaces or `<br>` for paragraph breaks — use blank lines. +- Each paragraph should focus on **one central idea**. +- Aim for **3–5 sentences** (100–200 words) per paragraph. Technical docs may stretch + to 300 words maximum; longer than that, split the paragraph. +- Use transitional sentences to connect consecutive paragraphs. + +### Line Breaks Within a Paragraph + +Avoid forced line breaks (`\` or two trailing spaces) within paragraphs. Rewrite +the sentence instead. If a hard break is genuinely needed, use a trailing backslash +(`\`) sparingly — it is more portable than two trailing spaces, which are invisible +and may be silently stripped by editors. + +```markdown +For some reason I really need a break here,\ +though it is probably not necessary. +``` + +--- + +## Capitalization + +Use the **original capitalisation** of all products, tools, and binaries: + +```markdown +# ✅ Correct +`Markdown`, `GitHub`, `macOS`, `Node.js`, `npm`, `PostgreSQL`, `OAuth` + +# ❌ Wrong +`markdown`, `Github`, `MacOS`, `node.js`, `NPM`, `Postgresql`, `oauth` +``` + +--- + +## Document Hygiene + +- **Delete stale content frequently** in small batches — a small accurate doc beats a + large outdated one. +- Identify what you truly need: release docs, API docs, testing guidelines. +- Avoid "documentation" in disrepair. Own your docs the same way you own your tests. +- When a document exceeds ~500 lines, split it into focused sub-documents with an index: + + ```markdown + ## Documentation Index + + 1. [Overview](./overview.md) — Concepts and introduction + 2. [Quick Start](./quickstart.md) — Up and running in 5 minutes + 3. [Tutorial](./tutorial.md) — Full walkthrough + 4. [API Reference](./api-reference.md) — Complete API reference + 5. [Troubleshooting](./troubleshooting.md) — Common issues and fixes + ``` diff --git a/template/.claude/skills/markdown/references/extended-syntax.md b/template/.claude/skills/markdown/references/extended-syntax.md new file mode 100644 index 0000000..7b8de6e --- /dev/null +++ b/template/.claude/skills/markdown/references/extended-syntax.md @@ -0,0 +1,325 @@ +# Extended syntax and platform flavours + +Covers: GitHub Flavored Markdown (GFM), GitLab Markdown, footnotes, task lists, +definition lists, heading IDs, emoji, highlight, math (LaTeX), Mermaid diagrams, +callout/alert blocks, automatic URLs, and platform compatibility notes. + +**Primary sources:** [GFM Spec](https://github.github.com/gfm/), +[Markdown Guide — Extended Syntax](https://www.markdownguide.org/extended-syntax/), +[GitLab Flavored Markdown](https://docs.gitlab.com/user/markdown/) + +> **Important:** Extended syntax is not universally supported. Always verify that +> your target platform supports a feature before using it. When in doubt, use basic +> syntax or an HTML fallback. + +--- + +## GitHub Flavored Markdown (GFM) + +GFM is the most widely used Markdown dialect. It extends CommonMark with: + +| Feature | Syntax | Notes | +|---|---|---| +| Task lists | `- [x]` / `- [ ]` | Interactive checkboxes | +| Tables | Pipe syntax | Native table support | +| Strikethrough | `~~text~~` | Double tildes | +| Autolinks | Bare URLs | Auto-converted to links | +| Disallowed raw HTML | — | Some HTML tags are sanitised | +| Emoji shortcodes | `:emoji:` | `:rocket:` → 🚀 | +| Footnotes | `[^1]` | Supported in newer GFM | +| Math | `$` and `$$` | LaTeX, added 2022 | +| Mermaid diagrams | ` ```mermaid ` | Flowcharts, sequences, etc. | +| Alerts / callouts | `> [!NOTE]` | 5 types, added 2023 | +| Collapsed sections | `<details>` | HTML, but rendered by GitHub | + +--- + +## GFM Alert Blocks (Callouts) + +The most important GFM extension for documentation. Five alert types: + +```markdown +> [!NOTE] +> Useful information the reader should know, even if skimming. + +> [!TIP] +> Optional advice that helps the reader succeed. + +> [!IMPORTANT] +> Key information necessary for the reader to succeed. + +> [!WARNING] +> Urgent information; the reader must take care. + +> [!CAUTION] +> Advises about risks or negative outcomes of an action. +``` + +GitHub renders each with a distinct coloured icon. Use these instead of plain +blockquotes whenever a semantic callout is needed. + +--- + +## Footnotes + +Footnotes add references and notes without cluttering body text. A superscript +link in the body jumps to the footnote definition at the bottom of the rendered page. + +```markdown +Here is a claim that needs a citation.[^1] + +This statement has a named footnote.[^note] + +[^1]: The source for the first claim. +[^note]: Named footnotes still render with sequential numbers, + but are easier to manage in source. Indent continuation lines + with 4 spaces to include multiple paragraphs. + + Second paragraph of the footnote. +``` + +### Footnote Rules + +- Identifiers can be numbers or words; no spaces or tabs. +- Identifiers are for source management only — output is always sequential numbers. +- Place footnote definitions at the end of the document or at the end of the section. +- Indent continuation lines with 4 spaces to include multi-paragraph footnotes. + +--- + +## Heading IDs (Custom Anchors) + +Override the auto-generated anchor for a heading: + +```markdown +## My Long Section Title {#short-id} + +Now link to it with: +[Go to short section](#short-id) +``` + +Useful when: +- The heading text is very long and produces an unwieldy anchor. +- You want to rename a heading without breaking existing deep links. +- You need stable anchor IDs that survive heading text changes. + +**Support:** Pandoc, Kramdown (Jekyll), MkDocs, many static site generators. +Not supported in plain GitHub rendering. + +--- + +## Task Lists + +See `02-formatting-syntax.md` for full task list syntax. Additional notes: + +- Task lists are **interactive** on GitHub (checkboxes are clickable in issues and PRs). +- In rendered documentation, they are read-only visual checkboxes. +- Nesting works: + + ```markdown + - [ ] Parent task + - [x] Completed sub-task + - [ ] Pending sub-task + ``` + +--- + +## Definition Lists + +Supported by Pandoc, MkDocs (with extensions), Kramdown, and PHP Markdown Extra. +**Not supported** in standard GFM. + +```markdown +First Term +: Definition of the first term. + +Second Term +: First definition of the second term. +: Second definition of the second term. + +*Markdown Term* +: Definition can contain **Markdown** formatting. +``` + +Rendered output: +> **First Term** — Definition of the first term. +> **Second Term** — First definition of the second term. / Second definition of the second term. + +--- + +## Math (LaTeX / KaTeX) + +Supported on GitHub (added 2022), GitLab, Jupyter, MkDocs with plugins, Pandoc. + +### Inline Math + +```markdown +The formula is $E = mc^2$ where $m$ is mass and $c$ is the speed of light. +``` + +### Block Math + +```markdown +$$ +\frac{d}{dx}\left( \int_{a}^{x} f(u)\,du\right)=f(x) +$$ +``` + +```markdown +$$ +\begin{pmatrix} +a & b \\ +c & d +\end{pmatrix} +$$ +``` + +--- + +## Mermaid Diagrams + +Supported on GitHub, GitLab, Notion, Obsidian, and many static site generators +(MkDocs with `pymdownx.superfences`). + +### Flowchart + +````markdown +```mermaid +flowchart LR + A[User Request] --> B{Authenticated?} + B -- Yes --> C[Load Dashboard] + B -- No --> D[Redirect to Login] +``` +```` + +### Sequence Diagram + +````markdown +```mermaid +sequenceDiagram + participant Browser + participant Server + participant DB + Browser->>Server: GET /api/user + Server->>DB: SELECT * FROM users + DB-->>Server: user rows + Server-->>Browser: 200 OK + JSON +``` +```` + +### Gantt Chart + +````markdown +```mermaid +gantt + title Project Timeline + dateFormat YYYY-MM-DD + section Planning + Requirements :done, 2024-01-01, 2024-01-07 + Design :active, 2024-01-08, 2024-01-14 + section Development + Backend :2024-01-15, 2024-01-28 + Frontend :2024-01-22, 2024-02-04 +``` +```` + +### Entity Relationship Diagram + +````markdown +```mermaid +erDiagram + USER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + PRODUCT ||--o{ LINE-ITEM : "included in" +``` +```` + +--- + +## Emoji Shortcodes (GFM) + +```markdown +:rocket: :white_check_mark: :warning: :x: :bulb: :books: +``` + +Renders as: 🚀 ✅ ⚠️ ❌ 💡 📚 + +Use emoji sparingly in technical documentation — they improve scannability for +status indicators but can appear unprofessional in formal docs. Never rely solely +on emoji to convey meaning (accessibility concern). + +--- + +## Strikethrough + +Standard GFM with double tildes: + +```markdown +~~deleted text~~ +``` + +**Do not** use single tildes (`~text~`) for strikethrough — single tildes are for +subscript in some parsers and strikethrough in others, causing inconsistent rendering. + +--- + +## Automatic URL Linking + +Most modern processors auto-link bare URLs: + +```markdown +Visit https://example.com for more information. +``` + +To suppress auto-linking, wrap in backticks: + +```markdown +`https://example.com/search?q=$TERM` +``` + +--- + +## Inline Diff (GitLab Only) + +```markdown +{+addition+} +{-deletion-} +``` + +Renders with green/red highlighting in GitLab. Not supported elsewhere. + +--- + +## Platform Compatibility Matrix + +| Feature | CommonMark | GFM (GitHub) | GitLab | Pandoc | Kramdown | MkDocs | +|---|:---:|:---:|:---:|:---:|:---:|:---:| +| Tables | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Task lists | ❌ | ✅ | ✅ | ✅ | ✅ | ✅* | +| Strikethrough | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Footnotes | ❌ | ✅ | ✅ | ✅ | ✅ | ✅* | +| Definition lists | ❌ | ❌ | ✅ | ✅ | ✅ | ✅* | +| Math (LaTeX) | ❌ | ✅ | ✅ | ✅ | ✅* | ✅* | +| Mermaid | ❌ | ✅ | ✅ | ❌ | ❌ | ✅* | +| GFM Alerts | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Custom heading IDs | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | +| Emoji shortcodes | ❌ | ✅ | ✅ | ✅* | ❌ | ✅* | +| Highlight (`==`) | ❌ | ❌ | ❌ | ✅* | ✅* | ✅* | +| Subscript (`~`) | ❌ | ❌ | ✅ | ✅* | ✅* | ✅* | +| Superscript (`^`) | ❌ | ❌ | ✅ | ✅* | ✅* | ✅* | + +`*` = requires plugin/extension configuration + +--- + +## Stack Overflow and Discord Notes + +**Stack Overflow** does not support: tables (use HTML `<table>`), task lists, footnotes. +Supports: headings, emphasis, lists, links, images, code blocks, blockquotes, horizontal rules. + +**Discord** supports: bold, italic, underline (`__text__`), strikethrough, inline code, +code blocks with syntax highlighting, blockquotes, spoiler tags (`||text||`), H1–H3. +Does not support: tables, images via Markdown, footnotes, task lists. +Note: Discord uses `__text__` for **underline**, not bold — conflicts with standard Markdown. + +**Slack** uses its own "mrkdwn" format — significantly different from standard Markdown. diff --git a/template/.claude/skills/markdown/references/file-management.md b/template/.claude/skills/markdown/references/file-management.md new file mode 100644 index 0000000..cfeac51 --- /dev/null +++ b/template/.claude/skills/markdown/references/file-management.md @@ -0,0 +1,347 @@ +# File management and documentation systems + +Covers: file naming conventions, repository folder structure, standard Markdown +files (README, CHANGELOG, CONTRIBUTING, etc.), versioning, commit conventions, +documentation system design, and maintenance. + +**Sources:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html), +[Folder Structure Conventions](https://github.com/kriasoft/Folder-Structure-Conventions), +[README — Wikipedia](https://en.wikipedia.org/wiki/README), +[Building a Markdown-Based Documentation System](https://medium.com/@rosgluk/building-a-markdown-based-documentation-system-72bef3cb1db3) + +--- + +## File Naming Conventions + +### Standard Repository Files (UPPERCASE) + +The following well-known files use UPPERCASE names by convention — this ensures +they sort near the top of ASCII-ordered directory listings and are immediately +recognisable: + +| File | Purpose | +|---|---| +| `README.md` | Project overview, installation, usage, badges | +| `CHANGELOG.md` | Version history and release notes | +| `CONTRIBUTING.md` | How to contribute to the project | +| `CODE_OF_CONDUCT.md` | Community behaviour standards | +| `SECURITY.md` | Vulnerability reporting policy | +| `LICENSE` | Licence text (plain text, no `.md` extension) | +| `SUMMARY.md` | GitBook / documentation table of contents | +| `SUPPORT.md` | Where to get help | + +These files should be placed in the **root directory** of the repository. + +### Non-Standard Documentation Files (lowercase-hyphenated) + +Any documentation that is not one of the standard files above should use +**lowercase letters with hyphens** as word separators: + +``` +docs/getting-started.md ✅ +docs/api-reference.md ✅ +docs/configuration-guide.md ✅ +docs/myAdditionalDoc.md ✅ (camelCase also acceptable for non-standard) + +docs/Getting Started.md ❌ (spaces break URLs) +docs/API_Reference.md ❌ (underscores less URL-friendly) +docs/APIreference.md ❌ (hard to read) +``` + +### Naming Rules + +- Use only **lowercase letters, digits, and hyphens** in non-standard file names. +- **No spaces** — spaces must be percent-encoded in URLs (`%20`) and break many tools. +- **No special characters**: `! @ # $ % ^ & * ( ) = + [ ] { } | \ ; ' " , < > ?` +- Use **printable ASCII only** — URI paths are case-sensitive; some file systems are not. +- Keep names **short and descriptive**: `getting-started.md` not `a-guide-to-getting-started-with-this-project.md`. +- Prefer **nouns** for reference docs and **verb phrases** for guides: + `configuration.md`, `deploy-to-production.md`. + +--- + +## Repository Folder Structure + +### Minimal Structure (small project) + +``` +project/ +├── README.md +├── CHANGELOG.md +├── CONTRIBUTING.md +├── LICENSE +└── docs/ + ├── getting-started.md + └── api-reference.md +``` + +### Standard Structure (medium project) + +``` +project/ +├── README.md +├── CHANGELOG.md +├── CONTRIBUTING.md +├── CODE_OF_CONDUCT.md +├── SECURITY.md +├── LICENSE +├── docs/ +│ ├── README.md ← docs index / overview +│ ├── getting-started.md +│ ├── configuration.md +│ ├── api/ +│ │ ├── README.md +│ │ └── endpoints.md +│ └── guides/ +│ ├── deploy.md +│ └── troubleshooting.md +├── assets/ +│ └── images/ +└── src/ +``` + +### Documentation-Only Repository or GitBook + +``` +docs/ +├── README.md ← introduction / overview +├── SUMMARY.md ← table of contents (GitBook) +├── chapter-1/ +│ ├── README.md ← chapter introduction +│ └── subsection.md +└── chapter-2/ + ├── README.md + └── subsection.md +``` + +### Rules + +- Store all non-standard docs under a `docs/` folder to avoid cluttering the root namespace. +- Each subdirectory in `docs/` should have its own `README.md` as an index/overview. +- Use short lowercase names for top-level directories except the standard files. +- `assets/` or `static/` for images, diagrams, and other media files. + +--- + +## Standard File Templates + +### README.md + +A project README should answer four questions a new visitor has: + +```markdown +# Project Name + +One-sentence description of what this project does and who it is for. + +## Features + +- Key feature one +- Key feature two +- Key feature three + +## Quick Start + +\```bash +npm install my-project +my-project --help +\``` + +## Documentation + +- [Getting Started](docs/getting-started.md) +- [API Reference](docs/api-reference.md) +- [Configuration](docs/configuration.md) + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## License + +[MIT](LICENSE) © Your Name +``` + +**README sections to consider (include what is relevant):** + +- Project name and tagline +- Badges (build status, coverage, version, licence) +- Description / what it does +- Features list +- Quick start / installation +- Usage examples (with code blocks) +- Documentation index / links +- Configuration reference (brief, or link out) +- Contributing guide link +- Acknowledgements / credits +- Licence + +### CHANGELOG.md + +Follow [Keep a Changelog](https://keepachangelog.com/) conventions: + +```markdown +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added +- New feature description + +## [2.1.0] — 2024-01-15 + +### Added +- User group management API +- Batch processing endpoint + +### Changed +- Improved authentication flow performance + +### Fixed +- Resolved race condition in session handling (#123) + +### Deprecated +- `GET /api/v1/users` — use `GET /api/v2/users` instead + +### Removed +- Legacy XML response format + +### Security +- Updated dependency X to patch CVE-2024-XXXX + +## [2.0.0] — 2024-01-01 + +### Changed +- **Breaking:** Refactored authentication API (see migration guide) + +[Unreleased]: https://github.com/org/repo/compare/v2.1.0...HEAD +[2.1.0]: https://github.com/org/repo/compare/v2.0.0...v2.1.0 +[2.0.0]: https://github.com/org/repo/releases/tag/v2.0.0 +``` + +**Change types (use consistently):** +`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security` + +### CONTRIBUTING.md + +```markdown +# Contributing to Project Name + +Thank you for considering contributing! + +## Ways to Contribute + +- Report bugs via [GitHub Issues](https://github.com/org/repo/issues) +- Suggest features or improvements +- Submit pull requests + +## Development Setup + +\```bash +git clone https://github.com/org/repo.git +cd repo +npm install +npm test +\``` + +## Pull Request Process + +1. Fork the repository and create a feature branch. +2. Write or update tests for your changes. +3. Ensure all tests pass: `npm test` +4. Update `CHANGELOG.md` under `[Unreleased]`. +5. Submit a pull request with a clear description. + +## Code of Conduct + +This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). +``` + +--- + +## Versioning Blocks in Documentation + +For API and library documentation, embed version information clearly: + +```markdown +--- +title: API Authentication Guide +version: 2.1.0 +last_updated: 2024-01-15 +author: Documentation Team +--- + +> **Version note:** This document applies to API v2.1.0 and above. +> For older versions, see the [archived documentation](./archive/). +``` + +--- + +## Commit Message Conventions for Documentation Changes + +``` +docs: update API authentication guide + +- Add OAuth 2.0 example with PKCE flow +- Fix broken code sample in section 3 +- Update related links + +Closes #123 +``` + +**Commit type prefixes:** + +| Prefix | Use for | +|---|---| +| `docs` | Documentation changes | +| `feat` | Documentation for a new feature | +| `fix` | Fix incorrect or broken documentation | +| `style` | Formatting-only changes (no content change) | +| `refactor` | Restructure documentation without content change | +| `chore` | Tooling, build, or configuration changes | + +--- + +## Documentation Versioning Strategy + +### Inline Versioning (small projects) + +Mark version applicability inline in the document: + +```markdown +> **Available since v1.4.0** + +> **Deprecated in v2.0.0** — use `newMethod()` instead. +``` + +### Changelog-Based (most projects) + +Maintain a single `CHANGELOG.md` as described above. + +### Versioned Subdirectories (large / enterprise projects) + +``` +docs/ +├── v2/ ← current version +│ └── api-reference.md +├── v1/ ← archived version +│ └── api-reference.md +└── README.md ← links to current version +``` + +--- + +## Documentation Maintenance Rules + +- **Delete stale content frequently** and in small batches — stale docs are worse than no docs. +- **Audit links periodically** — broken links erode reader trust. +- **Keep docs close to the code** they describe — co-located docs get updated when code changes. +- **Split documents** that exceed ~500 lines into focused sub-documents with an index. +- **Review documentation** as part of every pull request that changes behaviour. +- **Test code examples** in your docs (use CI where possible) — outdated examples mislead users. +- For docs systems: use GitHub Actions or similar CI to build, lint, and check links automatically. diff --git a/template/.claude/skills/markdown/references/formatting-syntax.md b/template/.claude/skills/markdown/references/formatting-syntax.md new file mode 100644 index 0000000..e9d3ee5 --- /dev/null +++ b/template/.claude/skills/markdown/references/formatting-syntax.md @@ -0,0 +1,306 @@ +# Formatting syntax + +Covers: emphasis (bold, italic, strikethrough), lists (ordered, unordered, nested, +task), blockquotes, horizontal rules, and line-break handling. + +**Primary sources:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html), +[Markdown Guide — Basic Syntax](https://www.markdownguide.org/basic-syntax/) + +--- + +## Emphasis + +### Bold + +Use `**double asterisks**` for bold (strong importance): + +```markdown +**bold text** +``` + +Use bold for: critical terms on first introduction, UI element labels, warnings, +key takeaways, and field/parameter names in documentation. + +### Italic + +Use `*single asterisks*` for italic (stress emphasis): + +```markdown +*italic text* +``` + +Use italic for: introducing new terms, titles of books/products, technical terms +used in a non-technical context, and light stress. + +### Bold + Italic + +```markdown +***bold and italic*** +``` + +Use only when both strong importance and stress emphasis are simultaneously needed. + +### Strikethrough + +```markdown +~~strikethrough~~ +``` + +Use for: text that is a deliberate error or has been superseded, deprecated +features, or crossed-off items in prose. + +```markdown +~~Old feature~~ replaced by new feature +Price: ~~$99~~ $79 +``` + +### Rules for Emphasis + +- **Always use asterisks**, not underscores. Underscores behave inconsistently inside + words across parsers (e.g., `some_variable_name` may render incorrectly): + + ```markdown + # ✅ Use asterisks + *italic* **bold** ***bold italic*** + + # ❌ Avoid underscores (unreliable mid-word) + _italic_ __bold__ + ``` + +- **Never bold or italicise entire paragraphs.** Overuse destroys emphasis. +- Use emphasis **sparingly** — every additional use dilutes the impact of all others. +- Do not use emphasis for purely decorative formatting. + +--- + +## Lists + +### When to Use Lists + +Use lists for genuinely enumerable or sequential items. Do **not** fragment flowing +prose into bullets — write prose instead. If all list items are a single short +phrase, a list is appropriate. If items need full sentences of context, consider +prose with subheadings. + +### Unordered Lists + +```markdown +- First item +- Second item +- Third item +``` + +Use `-` consistently throughout a file (do not mix `-`, `*`, and `+`). + +For single-line, non-nested lists, one space after the marker suffices: + +```markdown +- Foo +- Bar +- Baz +``` + +For wrapped text, use a 4-space total indent (3 spaces after `-`): + +```markdown +- Foo, which is a somewhat longer item that may wrap + to a second line and needs the 4-space indent. +- Bar. +``` + +### Ordered Lists + +For **short, stable** lists, use explicit sequential numbers (more readable in source): + +```markdown +1. First step +2. Second step +3. Third step +``` + +For **long or frequently changing** lists, use **lazy numbering** — Markdown renumbers +automatically, so you never have to renumber after inserting or removing an item: + +```markdown +1. First item +1. Second item +1. Third item +1. Fourth item +``` + +For ordered list items with wrapped text, use 2 spaces after the number: + +```markdown +1. First item — text starts at column 5 (4-space total indent). + Wrapped text also aligns here. +2. Second item. +``` + +### Nested Lists + +Use a **4-space indent** for both bullet and numbered nested lists: + +```markdown +1. Parent item (text at 4-space indent). + Wrapped text aligns here. + 1. Nested numbered item. + Wrapped text in nested list needs an 8-space indent. + 2. Another nested numbered item. +2. Back to parent level. + +- Bullet parent (text at 4-space indent). + - Nested bullet. + - Another nested bullet. +- Back to parent level. +``` + +**Do not mix inconsistent indentation** — it produces unpredictable rendering: + +```markdown +# ❌ Messy — avoid +* One space, +with no indent for wrapped text. + 1. Irregular nesting. +``` + +### Task Lists (GFM) + +Supported on GitHub, GitLab, and most modern Markdown renderers: + +```markdown +- [x] Complete project setup +- [x] Write documentation +- [ ] Add unit tests +- [ ] Deploy to production +``` + +Nested task lists: + +```markdown +- [ ] Top-level task + - [x] Sub-task one + - [ ] Sub-task two +``` + +--- + +## Blockquotes + +Use `>` to call out quoted text, important notes, or external attributions: + +```markdown +> This is a blockquote. It can span multiple lines. +> All lines should be prefixed with `>`. + +> First paragraph of the quote. +> +> Second paragraph of the quote (blank `>` line separates paragraphs). +``` + +### Nested Blockquotes + +```markdown +> Outer quote. +> +> > Nested quote inside the outer quote. +> +> Back to the outer quote. +``` + +### Blockquotes with Markdown Inside + +Blockquotes can contain any Markdown: + +```markdown +> **Note:** This feature requires Node.js 18 or higher. +> +> - Supported platforms: Linux, macOS, Windows +> - Minimum RAM: 512 MB +``` + +### Blockquote Usage Rules + +- Use to highlight important side-notes, warnings, and external quotations. +- Use **moderately** — overuse dilutes their visual effect. +- Do not use for generic indentation or visual styling. +- For formal callouts (Notes, Warnings, Tips), prefer GFM alert syntax when available + (see `05-extended-syntax.md`). + +--- + +## Horizontal Rules + +A horizontal rule is three or more hyphens, asterisks, or underscores on a line alone: + +```markdown +--- +*** +___ +``` + +### Usage Rules + +- Use `---` as the consistent choice (matches YAML front matter delimiters; distinctive). +- Use **sparingly** — only at **major structural transitions** (e.g., between an + introduction block and the main body). +- Do **not** use to separate every section — headings are the correct separator. +- Ensure a blank line before and after to avoid the `---` being parsed as a Setext H2. + +--- + +## Highlight, Subscript, Superscript + +These are extended syntax — not supported by all parsers. Always verify support +before using them. + +### Highlight + +```markdown +I need to highlight ==these very important words==. +``` + +### Subscript + +```markdown +H~2~O <!-- water --> +CO~2~ <!-- carbon dioxide --> +``` + +### Superscript + +```markdown +X^2^ <!-- x squared --> +E = mc^2^ +``` + +### Fallback HTML (when the above are unsupported) + +```markdown +H<sub>2</sub>O +X<sup>2</sup> +``` + +--- + +## Escaping Characters + +To display a literal Markdown character that would otherwise be interpreted as syntax, +prefix it with a backslash `\`: + +```markdown +\*Not italic\* +\# Not a heading +\[Not a link\] +\`Not code\` +\> Not a blockquote +``` + +Characters that can be escaped: +`\` `` ` `` `*` `_` `{}` `[]` `()` `#` `+` `-` `.` `!` `|` + +--- + +## Inline HTML + +Plain Markdown handles nearly all formatting needs. Use HTML only as a last resort. +See `04-tables-images-html.md` for the complete rules on HTML in Markdown. diff --git a/template/.claude/skills/markdown/references/tables-images-html.md b/template/.claude/skills/markdown/references/tables-images-html.md new file mode 100644 index 0000000..d507e19 --- /dev/null +++ b/template/.claude/skills/markdown/references/tables-images-html.md @@ -0,0 +1,277 @@ +# Tables, images, and HTML + +Covers: table syntax, alignment, best practices, when NOT to use tables; image +syntax, alt text, accessibility; HTML-in-Markdown rules. + +**Primary source:** [Google Markdown Style Guide](https://google.github.io/styleguide/docguide/style.html) + +--- + +## Tables + +### When to Use a Table + +Use tables **only** when data is genuinely two-dimensional with: +- Relatively uniform data distribution across both dimensions +- Many parallel items each with distinct, comparable attributes +- Content that benefits from at-a-glance scanning + +Tables are NOT appropriate when: +- Data could be a simple list (lists are easier to write and read) +- Several columns have the same value across rows +- Cells contain long prose +- There are very few rows relative to columns (or vice versa) +- The table has mostly empty cells + +**Example of data that should be a list, not a table (from the Google style guide):** + +```markdown +# ❌ Bad — this table has three specific problems +Fruit | Metrics | Grows on | Acute curvature | Attributes | Notes +------ | ------------ | -------- | ------------------ | ------------------- | ----- +Apple | Very popular | Trees | | Juicy, Firm, Sweet | Apples keep doctors away. +Banana | Very popular | Trees | 16 degrees average | Convenient, Soft | Most apes prefer mangoes. +``` + +The Google style guide names three specific table problems to watch for: + +1. **Poor distribution** — Several columns don't differ across rows, and some cells + are empty. This is usually a sign that the data may not benefit from tabular display. + +2. **Unbalanced dimensions** — There are very few rows relative to columns (or very + few columns relative to rows). When this ratio is unbalanced in either direction, + a table becomes little more than an inflexible format for text. + +3. **Rambling prose in cells** — Tables should tell a succinct story at a glance. + Long sentences or paragraphs inside cells defeat the purpose of a table. + +If any of these three problems apply, convert the table to a list with subheadings: + +```markdown +# ✅ Good — list form is more readable here +## Fruits + +Both are highly popular, sweet, and grow on trees. + +### Apple +- Juicy, firm +- Apples keep doctors away. + +### Banana +- Convenient, soft +- 16 degrees average acute curvature. +- Contrary to popular belief, most apes prefer mangoes. +``` + +### Table Syntax + +Use pipes `|` to separate columns and hyphens `---` to create the header separator: + +```markdown +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Cell | Cell | Cell | +| Cell | Cell | Cell | +``` + +The outer pipes on each row are optional but strongly recommended for consistency: + +```markdown +Column 1 | Column 2 ← harder to read without outer pipes +-------- | -------- +Cell | Cell +``` + +### Column Alignment + +Control alignment with colons in the separator row: + +```markdown +| Left-aligned | Centred | Right-aligned | +|:-------------|:-------:|--------------:| +| Default | Centred | Numbers | +| Text | Text | 12.50 | +| More text | More | 100.00 | +``` + +- `:---` — left align (default if no colon) +- `:---:` — centre align +- `---:` — right align (use for numeric columns) + +### Keeping Tables Readable + +- **Keep cells short** — Markdown offers no line-break within table cells. +- **Use reference links** for any URL inside a cell (see `03-code-and-links.md`). +- **Align pipe characters vertically** in source for readability: + + ```markdown + # ✅ Well-aligned source + | Transport | Favored by | Advantage | + |-----------|:-------------|:-------------------| + | Bicycle | Commuters | Zero emissions | + | Train | Travellers | High capacity | + | Bus | City riders | Frequent stops | + ``` + +- Cell widths in source don't need to match — Markdown renders them uniformly regardless: + + ```markdown + | Col | Col | + | --- | --- | + | Short | A longer cell that still renders fine | + ``` + +### Inline Formatting in Tables + +Bold, italic, inline code, and links all work inside table cells: + +```markdown +| Command | Description | +|----------------|-----------------------------------| +| `git status` | List **new or modified** files | +| `git diff` | Show *unstaged* file differences | +| `git commit` | [Commit staged changes][git-docs] | + +[git-docs]: https://git-scm.com/docs/git-commit +``` + +### Large Tables + +If a table is unavoidably wide, it is one of the few places where exceeding +the 80-character line limit is acceptable. Even so, use reference links to +minimise cell content. + +--- + +## Images + +### Syntax + +```markdown +![Alt text describing the image](path/to/image.png) + +<!-- With an optional title tooltip on hover --> +![Dashboard screenshot showing user statistics](images/dashboard.png "Main dashboard") + +<!-- Reference-style image (for long paths or reuse) --> +![Alt text][dashboard] + +[dashboard]: images/dashboard.png "Main dashboard" +``` + +### Clickable Images + +Wrap an image in a link to make it clickable: + +```markdown +[![Django logo — click to open documentation](img/django.png)](https://docs.djangoproject.com/) +``` + +### Alt Text Rules + +Alt text is **mandatory for all meaningful images**. It is used by: +- Screen readers for visually impaired users +- Browsers when the image fails to load +- Search engines for image indexing + +**Writing good alt text:** + +```markdown +# ✅ Good — describes what the image shows and why it matters +![Bar chart showing Q4 revenue up 23% compared to Q3](images/q4-revenue.png) +![Screenshot of the Settings > Privacy panel with Location toggle highlighted](images/privacy-settings.png) +![Flowchart: user request → auth check → cache lookup → database → response](images/request-flow.png) + +# ❌ Bad — uninformative +![image](images/q4-revenue.png) +![chart](images/chart1.png) +![screenshot](images/screen.png) +``` + +Guidelines for alt text: +- Describe **what the image shows** and **why it is relevant** in that context. +- Keep alt text concise — 1–2 sentences maximum. +- Do not start with "Image of…" or "Picture of…" (screen readers already announce it's an image). +- For **purely decorative** images (dividers, backgrounds), use empty alt text: `![](...)` + so screen readers skip them. +- For **complex diagrams** (architecture diagrams, flowcharts), provide a caption or + prose description in the surrounding text in addition to alt text. +- For **screenshots of UI**, describe the key element and what state it is in. + +### Image Storage + +- Store images in a dedicated `images/` or `assets/` subdirectory adjacent to the doc. +- For large documentation sites, use a top-level `assets/` or `static/` folder. +- **Prefer SVG** for diagrams and icons (scales without blurring, small file size). +- **Use PNG** for screenshots (lossless, renders text crisply). +- **Use JPEG** for photographs (efficient compression for photographic content). +- Use descriptive file names: `auth-flow-diagram.svg`, not `img1.png`. + +### When to Use Images + +- When it is genuinely **easier to show than to describe** (UI navigation, visual layouts). +- Architecture diagrams, flowcharts, and data visualisations that would be verbose as text. +- **Use sparingly** — excessive images distract, slow page load, and are inaccessible + without good alt text. When in doubt, write prose. + +--- + +## HTML in Markdown + +### Core Rule: Strongly Prefer Markdown + +Every HTML tag in a Markdown file reduces portability and readability. Some renderers +(e.g., Gitiles) ignore HTML entirely. Markdown meets almost all formatting needs +without HTML. + +> If you find yourself reaching for HTML, first ask: do I really need this? +> Can I restructure the content to express it in plain Markdown? + +### Acceptable HTML Uses (as a last resort) + +These are the narrow cases where HTML is permitted: + +```markdown +<!-- Hard line break when backslash isn't supported --> +Line one<br>Line two + +<!-- Subscript / superscript (when ~ and ^ aren't supported) --> +H<sub>2</sub>O +E = mc<sup>2</sup> + +<!-- Collapsible section (GFM, GitHub only) --> +<details> +<summary>Click to expand</summary> + +Collapsed content goes here. Full Markdown works inside. + +</details> + +<!-- Keyboard key styling --> +Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy. + +<!-- Inline colour — only on platforms that render it (e.g., GitHub) --> +<span style="color:red">Warning text</span> +``` + +### What HTML Must Never Be Used For + +- Page layout or multi-column layouts +- Font size, font family, or colour styling (except `<span>` for one-off critical warnings) +- `<b>` or `<i>` instead of `**` and `*` +- `<h1>`–`<h6>` instead of `#` syntax +- `<ul>`, `<ol>`, `<li>` instead of Markdown list syntax +- `<a href>` instead of Markdown link syntax +- `<img>` instead of `![]()` syntax +- `<table>` instead of pipe-table syntax (except when the table is genuinely too complex) + +### HTML Commenting + +HTML comments are invisible in rendered output and valid in all processors: + +```markdown +<!-- TODO: add diagram here --> +<!-- This section needs review by the security team --> +``` + +Use sparingly for author notes, reminders, or section markers not meant for readers. diff --git a/template/.claude/skills/pytest/SKILL.md b/template/.claude/skills/pytest/SKILL.md index 347cb47..638d14d 100644 --- a/template/.claude/skills/pytest/SKILL.md +++ b/template/.claude/skills/pytest/SKILL.md @@ -10,11 +10,11 @@ description: >- any request to add/improve/fix tests in a Python project. --- -# Writing pytest tests +# Pytest Skill -This skill covers everything you need to write clear, maintainable, and thorough pytest -test suites. It follows a progressive-disclosure structure: this file covers the -essentials, and the `references/` directory has deep dives on each topic. +A comprehensive skill for writing clear, maintainable, and thorough pytest test suites. +This file covers the essentials, and the `references/` directory has deep dives on each +topic. ## Quick reference: where to go deeper @@ -29,6 +29,13 @@ essentials, and the `references/` directory has deep dives on each topic. | Anti-patterns and fixes | [references/anti-patterns.md](references/anti-patterns.md) | | CI, coverage, and plugins | [references/ci-and-plugins.md](references/ci-and-plugins.md) | +**Bundled scripts** (in `scripts/` — run directly, no need to read them into context): + +| Script | What it does | +|------------------------|----------------------------------------------------------| +| `find_slow_tests.py` | Runs pytest, identifies tests exceeding a time threshold | +| `mark_slow_tests.py` | Adds `@pytest.mark.slow` to the identified slow tests | + Read the relevant reference file before working on a specific area. For a new test file, skim `test-organization.md` and `fixtures.md` first. For debugging flaky tests, start with `anti-patterns.md`. @@ -243,6 +250,73 @@ pytest --durations=10 # show 10 slowest tests --- +## Finding and marking slow tests + +Long-running tests slow down the feedback loop. The skill bundles two scripts that +work together to find slow tests and add `@pytest.mark.slow` so they can be skipped +during fast development iterations. + +### Step 1: Find slow tests + +Run the finder script from your project root. It executes pytest with `--durations=0` +and reports every test whose call phase exceeds the threshold (default: 1 second). + +```bash +python <skill-path>/scripts/find_slow_tests.py --threshold 1.0 +``` + +Options: + +- `--threshold 0.5` — flag tests slower than 500ms. +- `--top 10` — only show the 10 slowest. +- `--test-path tests/integration/` — limit to a specific directory. +- `--json-only` — machine-readable JSON on stdout only. + +The output is a JSON array on stdout (for piping) plus a human summary on stderr. + +### Step 2: Mark them + +Pipe the output directly into the marker script: + +```bash +python <skill-path>/scripts/find_slow_tests.py --threshold 1.0 \ + | python <skill-path>/scripts/mark_slow_tests.py +``` + +This adds `@pytest.mark.slow` above each slow test function and inserts +`import pytest` if the file does not already have it. Tests that already carry +the marker are skipped. + +Use `--dry-run` to preview changes without modifying files: + +```bash +python <skill-path>/scripts/find_slow_tests.py --threshold 1.0 \ + | python <skill-path>/scripts/mark_slow_tests.py --dry-run +``` + +### Step 3: Run fast tests only + +Once slow tests are marked, skip them during normal development: + +```bash +pytest -m "not slow" +``` + +Run the full suite (including slow tests) before opening a PR or in CI. + +### When to use this + +- After adding integration or E2E tests that touch real databases, networks, or + large datasets. +- When `pytest --durations=10` shows tests taking more than a second. +- During CI optimisation — split fast and slow test runs into separate jobs. + +Read [references/parametrize-and-markers.md](references/parametrize-and-markers.md) +for more on custom markers and [references/ci-and-plugins.md](references/ci-and-plugins.md) +for CI integration patterns. + +--- + ## Checklist before committing tests - [ ] Each test has a clear, descriptive name. @@ -251,5 +325,6 @@ pytest --durations=10 # show 10 slowest tests - [ ] Fixtures handle setup and teardown (no leftover state). - [ ] External I/O is mocked or uses `tmp_path`. - [ ] Parametrize is used where multiple inputs test the same logic. +- [ ] Slow tests (>1s) are marked with `@pytest.mark.slow`. - [ ] Coverage threshold is met for new and modified modules. - [ ] `just test` passes locally. diff --git a/template/.claude/skills/pytest/references/ci-and-plugins.md b/template/.claude/skills/pytest/references/ci-and-plugins.md index 05bfa96..293909f 100644 --- a/template/.claude/skills/pytest/references/ci-and-plugins.md +++ b/template/.claude/skills/pytest/references/ci-and-plugins.md @@ -184,6 +184,45 @@ pytest -n 4 # use exactly 4 workers - **Use `pytest -k`** to run a subset matching a keyword. - **Profile with `--durations=10`** to find the slowest tests and fixtures. - **Use `monkeypatch` instead of real I/O** where possible. +- **Mark and skip slow tests** during development — see below. + +### Automated slow-test detection + +The skill bundles scripts to automate slow-test management. Use them when +`--durations` reveals tests that consistently take over a second. + +**Find slow tests:** + +```bash +python <skill-path>/scripts/find_slow_tests.py --threshold 1.0 +``` + +**Mark them automatically:** + +```bash +python <skill-path>/scripts/find_slow_tests.py --threshold 1.0 \ + | python <skill-path>/scripts/mark_slow_tests.py +``` + +This adds `@pytest.mark.slow` to each slow function and ensures `import pytest` is +present. Use `--dry-run` on the marker script to preview changes first. + +**Split fast and slow runs in CI:** + +```yaml +# GitHub Actions — two parallel jobs +jobs: + fast-tests: + runs-on: ubuntu-latest + steps: + - run: pytest -m "not slow" -q + slow-tests: + runs-on: ubuntu-latest + steps: + - run: pytest -m "slow" -q +``` + +This keeps the fast feedback loop short while still running the full suite. ## Strict mode diff --git a/template/.claude/skills/pytest/scripts/find_slow_tests.py b/template/.claude/skills/pytest/scripts/find_slow_tests.py new file mode 100644 index 0000000..44b02f1 --- /dev/null +++ b/template/.claude/skills/pytest/scripts/find_slow_tests.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Find slow-running test functions by analysing pytest duration output. + +Runs pytest with --durations=0 (all tests) and reports any test whose duration +exceeds a configurable threshold. Output is a JSON array so other tools can +consume it, plus a human-readable summary on stderr. + +Usage: + python find_slow_tests.py # default 1.0s threshold + python find_slow_tests.py --threshold 0.5 # 500ms threshold + python find_slow_tests.py --threshold 2.0 --top 5 # top 5 above 2s + python find_slow_tests.py --json-only # suppress human summary +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import asdict, dataclass + + +@dataclass(frozen=True) +class SlowTest: + """A test that exceeded the duration threshold.""" + + nodeid: str + duration: float + file_path: str + function_name: str + line_hint: str + + +# pytest --durations output looks like: +# 1.23s call tests/test_core.py::test_heavy_computation +_DURATION_RE = re.compile( + r"^\s*(?P<duration>[\d.]+)s\s+(?P<phase>call|setup|teardown)\s+(?P<nodeid>.+)$" +) + + +def run_pytest_durations(test_path: str | None = None) -> str: + """Run pytest --durations=0 and return the raw stdout.""" + cmd = [sys.executable, "-m", "pytest", "--durations=0", "-q", "--tb=no", "--no-header"] + if test_path: + cmd.append(test_path) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + return result.stdout + + +def parse_durations(output: str, threshold: float) -> list[SlowTest]: + """Parse pytest duration lines and return tests above the threshold.""" + slow: list[SlowTest] = [] + + for line in output.splitlines(): + match = _DURATION_RE.match(line) + if not match: + continue + + # Only care about the 'call' phase — setup/teardown are fixture overhead + if match.group("phase") != "call": + continue + + duration = float(match.group("duration")) + if duration < threshold: + continue + + nodeid = match.group("nodeid").strip() + # nodeid format: tests/test_core.py::TestClass::test_method + # or: tests/test_core.py::test_function + parts = nodeid.split("::") + file_path = parts[0] if parts else nodeid + function_name = parts[-1] if len(parts) > 1 else "" + + # Build a grep-friendly hint: "file_path:def function_name" + line_hint = f"{file_path}:def {function_name}" + + slow.append( + SlowTest( + nodeid=nodeid, + duration=duration, + file_path=file_path, + function_name=function_name, + line_hint=line_hint, + ) + ) + + # Sort slowest first + slow.sort(key=lambda t: t.duration, reverse=True) + return slow + + +def main() -> None: + """Entry point for find_slow_tests.""" + parser = argparse.ArgumentParser( + description="Find pytest tests that exceed a duration threshold." + ) + parser.add_argument( + "--threshold", + type=float, + default=1.0, + help="Duration threshold in seconds (default: 1.0)", + ) + parser.add_argument( + "--top", + type=int, + default=0, + help="Only show top N slowest tests (0 = all, default: 0)", + ) + parser.add_argument( + "--test-path", + type=str, + default=None, + help="Path to a specific test file or directory (default: all tests)", + ) + parser.add_argument( + "--json-only", + action="store_true", + help="Output JSON only, suppress human-readable summary", + ) + args = parser.parse_args() + + output = run_pytest_durations(args.test_path) + slow_tests = parse_durations(output, args.threshold) + + if args.top > 0: + slow_tests = slow_tests[: args.top] + + # JSON on stdout (avoid print — ruff T201 in repo template) + sys.stdout.write(json.dumps([asdict(t) for t in slow_tests], indent=2) + "\n") + + # Human summary on stderr + if not args.json_only: + if not slow_tests: + sys.stderr.write(f"\n No tests exceeded {args.threshold}s threshold.\n") + else: + sys.stderr.write( + f"\n Found {len(slow_tests)} test(s) exceeding {args.threshold}s:\n\n" + ) + for t in slow_tests: + marker = " SLOW " if t.duration >= args.threshold * 2 else " slow " + sys.stderr.write(f"{marker} {t.duration:6.2f}s {t.nodeid}\n") + sys.stderr.write("\n") + + +if __name__ == "__main__": + main() diff --git a/template/.claude/skills/pytest/scripts/mark_slow_tests.py b/template/.claude/skills/pytest/scripts/mark_slow_tests.py new file mode 100644 index 0000000..870da52 --- /dev/null +++ b/template/.claude/skills/pytest/scripts/mark_slow_tests.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Add @pytest.mark.slow to test functions identified as slow. + +Reads a JSON array of slow tests (from find_slow_tests.py) and inserts the +@pytest.mark.slow decorator on each function that does not already have it. +Also ensures `import pytest` is present in modified files. + +Usage: + # Pipe from find_slow_tests.py + python find_slow_tests.py --threshold 1.0 | python mark_slow_tests.py + + # Or from a saved JSON file + python mark_slow_tests.py --input slow_tests.json + + # Dry-run to preview changes without writing + python mark_slow_tests.py --input slow_tests.json --dry-run +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class PendingMark: + """A test function that needs the @pytest.mark.slow decorator.""" + + file_path: str + function_name: str + duration: float + + +@dataclass +class FileEdit: + """Tracks all edits to be made to a single file.""" + + path: Path + marks_to_add: list[PendingMark] + + +def _stripped_lines(lines: list[str]) -> list[str]: + """Return lines without trailing newlines (for pattern matching).""" + return [ln.rstrip("\n") for ln in lines] + + +def find_function_line(lines: list[str], function_name: str) -> int | None: + """Find the line index of a def statement matching function_name. + + Returns the 0-based line index, or None if not found. + """ + pattern = re.compile(rf"^\s*(?:async\s+)?def\s+{re.escape(function_name)}\s*\(") + for i, line in enumerate(lines): + if pattern.match(line): + return i + return None + + +def already_has_slow_marker(lines: list[str], def_line: int) -> bool: + """Check whether the lines above a def already include @pytest.mark.slow.""" + # Walk backwards from the def line looking for decorators + i = def_line - 1 + while i >= 0: + stripped = lines[i].strip() + if stripped.startswith("@"): + if "pytest.mark.slow" in stripped: + return True + i -= 1 + continue + # If we hit a non-decorator, non-blank line, stop + if stripped and not stripped.startswith("#"): + break + i -= 1 + return False + + +def has_pytest_import(lines: list[str]) -> bool: + """Check whether the file already imports pytest.""" + for line in lines: + stripped = line.strip() + if stripped == "import pytest" or stripped.startswith("from pytest"): + return True + if re.match(r"^import\s+pytest\b", stripped): + return True + return False + + +def find_import_insert_line(lines: list[str]) -> int: + """Find the best line to insert 'import pytest'. + + Inserts after the last stdlib/third-party import block, or at the top of the + file (after docstrings and comments). + """ + last_import_line = -1 + for i, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith(("import ", "from ")): + last_import_line = i + + if last_import_line >= 0: + return last_import_line + 1 + + # No imports found — insert after initial comments/docstrings + for i, line in enumerate(lines): + stripped = line.strip() + if stripped and not stripped.startswith("#") and not stripped.startswith('"""'): + return i + + return 0 + + +def _plan_decorator_insertions( + lines: list[str], stripped: list[str], marks_to_add: list[PendingMark], path: Path +) -> tuple[dict[int, str], list[str]]: + """Compute line-index → decorator text and human-readable change lines.""" + changes: list[str] = [] + insertions: dict[int, str] = {} + + for mark in marks_to_add: + def_line = find_function_line(stripped, mark.function_name) + if def_line is None: + changes.append(f" SKIP {mark.function_name} in {path} (def not found)") + continue + + if already_has_slow_marker(stripped, def_line): + changes.append(f" SKIP {mark.function_name} in {path} (already marked slow)") + continue + + indent = re.match(r"^(\s*)", lines[def_line]).group(1) # type: ignore[union-attr] + insertions[def_line] = f"{indent}@pytest.mark.slow\n" + changes.append( + f" ADD @pytest.mark.slow → {mark.function_name} ({mark.duration:.2f}s) in {path}" + ) + + return insertions, changes + + +def _write_file_with_import_and_decorators( + path: Path, + lines: list[str], + stripped: list[str], + insertions: dict[int, str], + dry_run: bool, +) -> list[str]: + """Build new file content with optional import and decorators; return extra changes.""" + extra: list[str] = [] + needs_import = not has_pytest_import(stripped) + import_line = find_import_insert_line(stripped) if needs_import else -1 + + new_lines: list[str] = [] + import_inserted = False + + for i, line in enumerate(lines): + if needs_import and i == import_line and not import_inserted: + new_lines.append("import pytest\n") + import_inserted = True + extra.append(f" ADD import pytest → {path}") + + if i in insertions: + new_lines.append(insertions[i]) + + new_lines.append(line) + + if not dry_run: + path.write_text("".join(new_lines), encoding="utf-8") + + return extra + + +def apply_marks(file_edit: FileEdit, dry_run: bool = False) -> list[str]: + """Apply @pytest.mark.slow to the identified functions in a file. + + Returns a list of human-readable change descriptions. + """ + path = file_edit.path + if not path.exists(): + return [f" SKIP {path} (file not found)"] + + lines = path.read_text(encoding="utf-8").splitlines(keepends=True) + stripped = _stripped_lines(lines) + + insertions, changes = _plan_decorator_insertions(lines, stripped, file_edit.marks_to_add, path) + + if not insertions: + return changes + + extra = _write_file_with_import_and_decorators(path, lines, stripped, insertions, dry_run) + return changes + extra + + +def main() -> None: + """Entry point for mark_slow_tests.""" + parser = argparse.ArgumentParser(description="Add @pytest.mark.slow to slow test functions.") + parser.add_argument( + "--input", + type=str, + default=None, + help="Path to JSON file from find_slow_tests.py (default: read stdin)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would change without modifying files", + ) + args = parser.parse_args() + + raw = Path(args.input).read_text(encoding="utf-8") if args.input else sys.stdin.read() + + try: + slow_tests = json.loads(raw) + except json.JSONDecodeError as e: + sys.stderr.write(f"Error: could not parse JSON input: {e}\n") + sys.exit(1) + + if not slow_tests: + sys.stderr.write("No slow tests to mark.\n") + sys.exit(0) + + # Group by file + file_edits: dict[str, FileEdit] = {} + for entry in slow_tests: + fp = entry["file_path"] + if fp not in file_edits: + file_edits[fp] = FileEdit(path=Path(fp), marks_to_add=[]) + file_edits[fp].marks_to_add.append( + PendingMark( + file_path=fp, + function_name=entry["function_name"], + duration=entry["duration"], + ) + ) + + # Apply marks + all_changes: list[str] = [] + for file_edit in file_edits.values(): + all_changes.extend(apply_marks(file_edit, dry_run=args.dry_run)) + + # Report + mode = "DRY RUN" if args.dry_run else "APPLIED" + sys.stderr.write(f"\n [{mode}] @pytest.mark.slow changes:\n\n") + for change in all_changes: + sys.stderr.write(f"{change}\n") + sys.stderr.write("\n") + + added = sum(1 for c in all_changes if c.startswith(" ADD @pytest")) + skipped = sum(1 for c in all_changes if c.startswith(" SKIP")) + sys.stderr.write(f" Summary: {added} marker(s) added, {skipped} skipped\n") + + +if __name__ == "__main__": + main() diff --git a/template/.claude/skills/python-code-quality/SKILL.md b/template/.claude/skills/python-code-quality/SKILL.md new file mode 100644 index 0000000..023c24d --- /dev/null +++ b/template/.claude/skills/python-code-quality/SKILL.md @@ -0,0 +1,113 @@ +--- +name: python-code-quality +description: >- + Use this skill whenever the task involves Python code style, formatting, linting, type + checking, security scanning, or commit hygiene in this project. Triggers include: + configuring or running ruff (lint or format), setting up or modifying pre-commit hooks, + configuring or running basedpyright, running bandit or semgrep, fixing lint/type/security + errors, writing or updating pyproject.toml quality sections, CI pipeline steps for code + quality, or any question like "how do I enforce formatting", "why is ruff complaining", + "pre-commit isn't running", "type error I don't understand", or "security finding I need + to suppress". Also use proactively when writing new Python files that will need to pass + CI checks. +--- + +# Python Code Quality Skill + +This project enforces code quality through five tools: + +| Tool | Role | Config location | +|---|---|---| +| **ruff** | Lint + format (replaces flake8 / isort / black) | `[tool.ruff]` in `pyproject.toml` | +| **pre-commit** | Git hook runner — gates every commit | `.pre-commit-config.yaml` | +| **basedpyright** | Static type checker (strict pyright fork) | `[tool.basedpyright]` in `pyproject.toml` | +| **bandit** | Security linter — fixed Python vulnerability patterns | `[tool.bandit]` in `pyproject.toml` | +| **semgrep** | Pattern-based scanner + custom rules | `.semgrep.yml` | + +--- + +## Quick reference + +### Run everything locally + +```bash +pre-commit run --all-files # run all hooks on every file (recommended) + +# Or run each tool individually: +ruff format . # auto-format +ruff check --fix . # lint + auto-fix safe issues +basedpyright # type-check +bandit -c pyproject.toml -r src/ # security lint +semgrep --config .semgrep.yml src/ # pattern scan +``` + +### Common failures and fast fixes + +| Symptom | Likely cause | Fix | +|---|---|---| +| `ruff` fails on unused import | `F401` rule | Remove import or `# noqa: F401` for intentional re-exports | +| `ruff format` changes file | File not formatted | Run `ruff format .` and re-stage | +| pre-commit hook not running | Hook not installed | Run `pre-commit install` | +| pre-commit passes locally, fails in CI | Staged-only vs all-files mismatch | Run `pre-commit run --all-files` before pushing | +| basedpyright `reportUnknownVariableType` | Missing annotation | Add type annotation; see `references/basedpyright.md` | +| basedpyright `reportMissingImports` | Package not in venv | Install package or add stub; see `references/basedpyright.md` | +| bandit `B[code]` finding | Security anti-pattern | Fix or add `# nosec B<code>` with explanation | +| semgrep finding | Code matches security pattern | Fix or add `# nosemgrep: <rule-id>` inline | + +### CI ordering (fast-fail principle) + +Run cheapest checks first so failures surface quickly without wasting time on later stages: + +``` +ruff format --check → ruff check → bandit → semgrep → basedpyright → pytest +``` + +| Stage | Why here | +|---|---| +| `ruff format --check` | Instant — no point type-checking badly formatted code | +| `ruff check` | Fast Rust linter — catches style and logic issues cheaply | +| `bandit` | Fast fixed-rule security scan — pure AST analysis | +| `semgrep` | Slower than bandit (fetches registry rules); runs after fast checks | +| `basedpyright` | Needs full import graph resolved — slower, but before tests | +| `pytest` | Most expensive — only run when everything above passes | + +--- + +## Quick reference: where to go deeper + +Read the relevant reference file for configuration details, error code explanations, +or CI integration specifics: + +| Topic | Reference file | +|---------------------------------|----------------------------------------------------------------------| +| ruff (lint + format) | [references/ruff.md](references/ruff.md) | +| pre-commit hooks | [references/pre-commit.md](references/pre-commit.md) | +| basedpyright (type checking) | [references/basedpyright.md](references/basedpyright.md) | +| bandit (security linting) | [references/bandit.md](references/bandit.md) | +| semgrep (pattern scanning) | [references/semgrep.md](references/semgrep.md) | +| Complete config files | [references/complete-configs.md](references/complete-configs.md) | + +--- + +## Adding a new tool + +1. Create `references/<toolname>.md` using the template below. +2. Add a row to the tools table at the top of this file. +3. Add the tool's hook to `.pre-commit-config.yaml` (see `references/pre-commit.md`). +4. Add the tool's `pyproject.toml` section to `references/complete-configs.md`. +5. Insert the tool into the CI ordering table above with a rationale for placement. + +### New tool reference template + +```markdown +# <ToolName> + +## What it does +## Installation +## pyproject.toml config (annotated) +## Common error codes and fixes +## Running <toolname> +## CI step +## Pre-commit hook entry +## Gotchas +``` diff --git a/template/.claude/skills/python-code-quality/references/bandit.md b/template/.claude/skills/python-code-quality/references/bandit.md new file mode 100644 index 0000000..a2df6a8 --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/bandit.md @@ -0,0 +1,171 @@ +# Bandit + +Bandit is a security linter for Python. It scans source code for common security issues +using a set of AST-based plugins, each targeting a specific vulnerability class. + +--- + +## What it does + +- Statically analyses Python AST for security anti-patterns +- Reports each finding with a severity (LOW / MEDIUM / HIGH) and a confidence + (LOW / MEDIUM / HIGH) so you can filter by signal strength +- Does **not** execute code — pure static analysis + +--- + +## Installation + +```bash +pip install bandit # or: uv add --dev bandit +``` + +Verify: +```bash +bandit --version +``` + +--- + +## pyproject.toml config (annotated) + +Bandit reads a limited set of keys from `[tool.bandit]`. Note that severity/confidence +thresholds must be passed as **CLI flags** — they are not supported as pyproject.toml +keys. + +```toml +[tool.bandit] +# Directories or files to scan (passed to -r). +targets = ["src"] + +# Test IDs to skip project-wide. Use sparingly — prefer per-line nosec. +# B101 = assert statement (valid in tests; exclude tests dir instead of skipping globally) +# B104 = binding to 0.0.0.0 (acceptable in containerised services — document the reason) +skips = ["B101"] + +# Paths to exclude from scanning (regex matched against file path). +exclude_dirs = [".venv", "build", "dist", "tests"] +``` + +### Severity and confidence thresholds + +Thresholds are set via CLI flags, not pyproject.toml: + +| CLI flag | Meaning | +|---|---| +| `-l` | Report LOW+ severity (default: all) | +| `-ll` | Report MEDIUM+ severity (recommended) | +| `-lll` | Report HIGH severity only | +| `-i` | Report LOW+ confidence | +| `-ii` | Report MEDIUM+ confidence (recommended) | +| `-iii` | Report HIGH confidence only | + +For CI, use `-ll -ii` (MEDIUM/MEDIUM) to filter noise without missing real issues: + +```bash +bandit -c pyproject.toml -r src/ -ll -ii +``` + +--- + +## Common issue codes and fixes + +| Code | Issue | Fix | +|---|---|---| +| `B101` | `assert` statement | Use explicit `if`/`raise` for runtime validation | +| `B105` | Hardcoded password — string literal | Load from env var or secrets manager | +| `B106` | Hardcoded password — function argument default | Use `None` default; load at runtime | +| `B107` | Hardcoded password — function argument | Same as B106 | +| `B108` | Probable insecure temp file | Use `tempfile.mkstemp()` or `tempfile.TemporaryFile()` | +| `B110` | `try/except/pass` — silenced exception | Log or handle the exception explicitly | +| `B201` | Flask debug mode in production | Never set `debug=True` outside local dev | +| `B301` | `pickle` on untrusted data | Use `json` or `msgpack` for untrusted sources | +| `B303` | MD5 / SHA1 for cryptography | Use `hashlib.sha256()` or better | +| `B311` | `random` for security-sensitive use | Use the `secrets` module instead | +| `B324` | Insecure hash function | Use SHA-256+ | +| `B501` | SSL certificate verification disabled | Remove `verify=False` | +| `B601` | `shell=True` in subprocess | Pass a list of args; never interpolate user input | +| `B602` | `subprocess` with shell injection risk | Same as B601 | +| `B608` | SQL string formatting — injection risk | Use parameterised queries | + +### Suppressing a finding inline + +```python +result = subprocess.run(cmd, shell=True) # nosec B602 +``` + +Always include the specific code in `# nosec`. A bare `# nosec` suppresses every finding +on the line and makes the reasoning invisible to reviewers. + +To suppress a finding project-wide, add it to `skips` in `pyproject.toml` with a comment +explaining why — e.g. `skips = ["B104"] # service runs in a container; 0.0.0.0 is correct`. + +--- + +## Running bandit + +```bash +# Basic scan using pyproject.toml config: +bandit -c pyproject.toml -r src/ + +# Recommended CI mode — MEDIUM severity and confidence minimum: +bandit -c pyproject.toml -r src/ -ll -ii + +# JSON output for downstream tooling or artefact storage: +bandit -c pyproject.toml -r src/ -f json -o bandit-report.json + +# Run only specific tests: +bandit -r src/ -t B301,B303 +``` + +Exit codes: `0` = no issues at or above threshold, `1` = issues found, `2` = bandit error. + +--- + +## CI step (GitHub Actions) + +```yaml +- name: Install bandit + run: pip install bandit + +- name: Security lint with bandit + run: bandit -c pyproject.toml -r src/ -ll -ii +``` + +See `SKILL.md` for the full CI ordering across all tools. + +--- + +## Pre-commit hook entry + +```yaml +- repo: https://github.com/PyCQA/bandit + rev: 1.8.3 # pin to a specific release; update with: pre-commit autoupdate + hooks: + - id: bandit + args: ["-c", "pyproject.toml", "-ll", "-ii"] + # Scope to src/ only — test code legitimately uses assert, subprocess, etc. + files: ^src/ +``` + +--- + +## Gotchas + +- **Exclude tests from scanning.** Test code legitimately uses `assert` (B101), raw + subprocess calls, and sometimes intentionally insecure patterns in fixtures. Add + `tests` to `exclude_dirs` in `[tool.bandit]` rather than globally skipping B101. +- **Severity thresholds are CLI-only.** You cannot set `severity = "MEDIUM"` in + `[tool.bandit]` — bandit ignores unknown keys. Pass `-ll -ii` on the command line + (or in the pre-commit `args`). +- **`# nosec` without a code is an anti-pattern.** Always write `# nosec B601` so + reviewers know exactly which finding is being suppressed and why. +- **Bandit flags patterns, not intent.** A `random` call used for a non-security shuffle + will still trigger B311. Add `# nosec B311` with a comment explaining the non-security + use rather than globally skipping B311. +- **Bandit is not a substitute for a full security audit.** It catches common + anti-patterns but misses logic-level vulnerabilities, dependency CVEs (use `pip-audit` + for those), and runtime issues. +- **Overlap with semgrep.** Both tools flag issues like `eval`, insecure hashes, and SQL + injection. The overlap is intentional — each catches slightly different forms of the + same pattern. Do not remove bandit in favour of semgrep. diff --git a/template/.claude/skills/python-code-quality/references/basedpyright.md b/template/.claude/skills/python-code-quality/references/basedpyright.md new file mode 100644 index 0000000..e8fe363 --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/basedpyright.md @@ -0,0 +1,247 @@ +# basedpyright + +basedpyright is a community fork of pyright (Microsoft's static type checker for Python). +It adds stricter defaults, clearer error messages, and additional rules over vanilla pyright. + +--- + +## What it does + +- Performs static type analysis — no code is executed +- Checks annotations, inferred types, call signatures, narrowing, exhaustiveness, and more +- Reads `[tool.basedpyright]` from `pyproject.toml` + +--- + +## Installation + +```bash +pip install basedpyright # or: uv add --dev basedpyright +``` + +Verify: +```bash +basedpyright --version +``` + +--- + +## pyproject.toml config (annotated) + +```toml +[tool.basedpyright] +# Python version used for type checking (e.g. affects which stdlib types are available). +pythonVersion = "3.11" + +# First-party source directories. Only these paths are type-checked. +include = ["src"] + +# Paths excluded from analysis. +exclude = ["**/__pycache__", ".venv", "build", "dist"] + +# Virtual environment resolution. +# venvPath = the DIRECTORY that contains your venv folder (usually the project root). +# venv = the NAME of the venv folder inside venvPath. +# Together they tell basedpyright where to find installed third-party packages. +# Example: venvPath = "." and venv = ".venv" resolves to ./.venv +venvPath = "." +venv = ".venv" + +# ── Strictness level ──────────────────────────────────────────────────────── +# Options: "off" | "basic" | "standard" | "strict" | "all" +# Recommendation: start at "standard", move to "strict" once the codebase is annotated. +typeCheckingMode = "standard" + +# ── Individual rule overrides ──────────────────────────────────────────────── +# Each rule can be set to "none" | "information" | "warning" | "error" +# independently of typeCheckingMode. Uncomment to customise: + +# reportUnknownVariableType = "error" # flag x: Unknown +# reportUnknownMemberType = "error" # flag unknown attribute types +# reportMissingTypeArgument = "warning" # flag bare list, dict etc. +# reportUnusedImport = "warning" # overlaps with ruff F401 +# reportUninitializedInstanceVariable = "warning" +``` + +### Strictness levels explained + +| Level | What it checks | +|---|---| +| `off` | Type checking disabled | +| `basic` | Only obvious errors: undefined names, wrong argument counts | +| `standard` | Full pyright checks — good default for most projects | +| `strict` | All checks; full annotation coverage required | +| `all` | basedpyright-specific extras on top of `strict` (may have false positives) | + +**Recommended progression:** start at `standard`, fix all errors, then move to `strict`. +Use per-file overrides (below) to keep `strict` globally while granting exceptions for +tests or legacy modules. + +--- + +## Per-file overrides + +Relax or tighten type checking for specific directories: + +```toml +[tool.basedpyright] +typeCheckingMode = "strict" + +[[tool.basedpyright.executionEnvironments]] +root = "tests" +typeCheckingMode = "basic" # test fixtures are often untyped; relax here + +[[tool.basedpyright.executionEnvironments]] +root = "scripts" +typeCheckingMode = "standard" +``` + +For a single line, use an inline ignore (last resort — prefer fixing the root cause): + +```python +result = some_untyped_library.call() # type: ignore[no-untyped-call] +``` + +Always include the specific error code in `type: ignore`. Bare `# type: ignore` silences +all errors on the line and makes the intent invisible to reviewers. + +--- + +## Common error codes and fixes + +| Code | Meaning | Fix | +|---|---|---| +| `reportMissingImports` | Package not installed in the venv | Install the package; or check `venvPath`/`venv` config | +| `reportMissingTypeStubs` | Package has no type information | Install `types-<pkg>` stub; see below | +| `reportUnknownVariableType` | Type inferred as `Unknown` | Add an explicit annotation | +| `reportUnknownMemberType` | Attribute type is `Unknown` | Annotate the class attribute | +| `reportUnknownArgumentType` | Argument type cannot be inferred | Annotate the caller or the callee | +| `reportReturnType` | Return value doesn't match declared return type | Fix annotation or return value | +| `reportAttributeAccessIssue` | Attribute doesn't exist on the type | Check spelling; use `hasattr` guard | +| `reportOperatorIssue` | Operator not defined for these types | Narrow the type before the operation | +| `reportIndexIssue` | Index or key type is invalid | Ensure key type matches the container | +| `reportCallIssue` | Object is not callable | Check the type; unwrap Optional before calling | + +### Handling third-party libraries without type stubs + +**Option 1 — Install community stubs** (preferred when stubs exist): + +```bash +pip install types-requests types-PyYAML types-python-dateutil +``` + +**Option 2 — Suppress missing-stubs warnings project-wide** (when no stubs exist): + +```toml +[tool.basedpyright] +reportMissingTypeStubs = "none" # or "warning" to see it without failing CI +``` + +**Option 3 — Suppress per import** (for one-off cases): + +```python +import untyped_lib # type: ignore[import-untyped] +``` + +**Option 4 — Generate a stub skeleton**: + +```bash +basedpyright --createstub some_package # writes a stub to typestubs/some_package/ +``` + +Commit the generated stubs to the repo and add `stubPath = "typestubs"` to +`[tool.basedpyright]` so basedpyright picks them up automatically. + +--- + +## Running basedpyright + +```bash +# Check entire project (reads pyproject.toml automatically): +basedpyright + +# Check a specific file: +basedpyright src/mymodule.py + +# Verbose output — shows which config file was loaded, useful for debugging: +basedpyright --verbose + +# JSON output — for tooling integration or counting errors: +basedpyright --outputjson | python -c "import sys,json; d=json.load(sys.stdin); print(d['summary'])" + +# Count current errors (useful for tracking incremental adoption progress): +basedpyright 2>&1 | grep " error" | wc -l +``` + +Exit codes: `0` = no errors, `1` = type errors found, `2` = fatal configuration error. + +--- + +## Incremental adoption strategy + +For an existing codebase with many type errors, adopt in stages: + +1. **`typeCheckingMode = "basic"`** — fix the small set of obvious errors first. +2. **`typeCheckingMode = "standard"`** — fix errors, use `executionEnvironments` to + relax specific directories (tests, scripts, legacy modules) temporarily. +3. **`typeCheckingMode = "strict"`** — requires full annotation coverage across `src/`. + Tackle module by module. Add `# type: ignore[<code>]` as a last resort. +4. **`typeCheckingMode = "all"`** — evaluate locally; some rules may be too aggressive + for production CI. Keep at `"strict"` in CI unless all `"all"` rules are clean. + +--- + +## CI step (GitHub Actions) + +```yaml +- name: Install dependencies + run: pip install -r requirements.txt # basedpyright must be in here, or: + # run: uv sync + +- name: Type check with basedpyright + run: basedpyright +``` + +basedpyright needs the project's dependencies installed so it can resolve third-party +imports. If it reports `reportMissingImports` on every import, the venv is not being +found — check that `venvPath` and `venv` in `pyproject.toml` match the actual venv path, +or that the CI runner has the packages on `PATH`. + +--- + +## Pre-commit hook entry + +Run as a `local` hook to use the project's installed venv: + +```yaml +- repo: local + hooks: + - id: basedpyright + name: basedpyright + entry: basedpyright + language: system # uses basedpyright from the active venv + types: [python] + pass_filenames: false # analyses the whole project, not individual files +``` + +Requires `basedpyright` to be installed in the active environment before committing. +In CI, install dependencies before running pre-commit (see `references/pre-commit.md`). + +--- + +## Gotchas + +- **`venvPath` vs `venv` — a common confusion.** `venvPath` is the *directory that + contains* the venv, and `venv` is the *name of the venv folder*. For a project at + `/my/project` with a venv at `/my/project/.venv`, set `venvPath = "."` and + `venv = ".venv"`. Setting `venvPath = ".venv"` is wrong. +- **`reportMissingImports` on all third-party imports** usually means the venv isn't + found. Double-check `venvPath`/`venv` config or confirm packages are installed. +- **basedpyright vs pyright.** A colleague using vanilla pyright will see nearly + identical errors. basedpyright adds rules like `reportUnreachable`. Both tools read + the same `[tool.basedpyright]` / `[tool.pyright]` config keys. +- **`pass_filenames: false` is required in pre-commit.** basedpyright resolves the full + import graph across all files; giving it individual filenames breaks cross-module + type inference. +- **`typeCheckingMode = "all"` may produce false positives.** Use `"strict"` in CI + and evaluate `"all"` locally before committing to it. diff --git a/template/.claude/skills/python-code-quality/references/complete-configs.md b/template/.claude/skills/python-code-quality/references/complete-configs.md new file mode 100644 index 0000000..bda78c2 --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/complete-configs.md @@ -0,0 +1,257 @@ +# Complete configurations + +Ready-to-use configuration files for all tools in this project. Copy these into +your project and adjust versions, paths, and rule selections as needed. + +--- + +## pyproject.toml — all tool sections + +```toml +# ── Ruff ──────────────────────────────────────────────────────────────────── +[tool.ruff] +target-version = "py311" +src = ["src"] +exclude = [".git", ".venv", "__pycache__", "build", "dist"] + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "UP", "B", "SIM"] +ignore = ["E501"] +fixable = ["ALL"] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["PLR2004", "F401"] +"**/__init__.py" = ["F401"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" +line-length = 88 + +# ── basedpyright ───────────────────────────────────────────────────────────── +[tool.basedpyright] +pythonVersion = "3.11" +include = ["src"] +exclude = ["**/__pycache__", ".venv", "build", "dist"] +venvPath = "." +venv = ".venv" +typeCheckingMode = "standard" + +# ── Bandit ─────────────────────────────────────────────────────────────────── +[tool.bandit] +targets = ["src"] +skips = [] # add rule IDs here only with a comment explaining why +exclude_dirs = [".venv", "build", "dist", "tests"] +# Note: severity/confidence thresholds are CLI flags (-ll -ii), not pyproject keys. +``` + +--- + +## .pre-commit-config.yaml — all hooks + +```yaml +minimum_pre_commit_version: "3.0.0" + +default_language_version: + python: python3.11 + +repos: + # ── Ruff: lint + format ────────────────────────────────────────────────── + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format + + # ── Bandit: security lint ───────────────────────────────────────────────── + - repo: https://github.com/PyCQA/bandit + rev: 1.8.3 + hooks: + - id: bandit + args: ["-c", "pyproject.toml", "-ll", "-ii"] + files: ^src/ + + # ── Semgrep: pattern-based security scan ───────────────────────────────── + - repo: https://github.com/semgrep/semgrep + rev: v1.60.0 + hooks: + - id: semgrep + args: ["--config", "p/python", "--config", ".semgrep.yml", "--severity", "ERROR"] + files: ^src/ + + # ── basedpyright: type checking ────────────────────────────────────────── + # Requires basedpyright to be installed in the active venv. + - repo: local + hooks: + - id: basedpyright + name: basedpyright + entry: basedpyright + language: system + types: [python] + pass_filenames: false + + # ── General hygiene ────────────────────────────────────────────────────── + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: debug-statements +``` + +--- + +## .semgrep.yml — local custom rules + +```yaml +# .semgrep.yml +# Local project-specific rules. Registry packs (p/python etc.) are passed +# as --config flags on the CLI, not listed here. +# +# Usage: +# semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/ + +rules: + - id: no-eval + pattern: eval(...) + message: > + eval() executes arbitrary code. Use ast.literal_eval() for safe data + parsing, or refactor to avoid dynamic evaluation entirely. + languages: [python] + severity: ERROR + + - id: no-hardcoded-secrets-in-jwt + patterns: + - pattern: jwt.encode(..., "$SECRET", ...) + - pattern-not: jwt.encode(..., os.environ.get(...), ...) + message: Hardcoded JWT secret. Load from environment variable or secrets manager. + languages: [python] + severity: ERROR +``` + +--- + +## GitHub Actions workflow — full CI pipeline + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + # ── Fast checks: format + lint + security ─────────────────────────────── + quality: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: Install quality tools + run: pip install ruff bandit semgrep + + - name: Ruff — format check + run: ruff format --check . + + - name: Ruff — lint + run: ruff check . + + - name: Bandit — security lint + run: bandit -c pyproject.toml -r src/ -ll -ii + + - name: Cache semgrep rules + uses: actions/cache@v4 + with: + path: ~/.semgrep/cache + key: semgrep-${{ hashFiles('.semgrep.yml') }} + + - name: Semgrep — pattern scan + run: | + semgrep --config p/python \ + --config p/owasp-top-ten \ + --config .semgrep.yml \ + --severity ERROR \ + src/ + + # ── Type checking ──────────────────────────────────────────────────────── + typecheck: + name: Type Check + runs-on: ubuntu-latest + needs: quality # only run if quality checks pass + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: Install dependencies + run: | + pip install -r requirements.txt # must include basedpyright + + - name: basedpyright — type check + run: basedpyright + + # ── Tests ──────────────────────────────────────────────────────────────── + test: + name: Tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + needs: [quality, typecheck] # only run if both upstream jobs pass + + strategy: + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run pytest with coverage + run: pytest --maxfail=1 --disable-warnings --cov=src --cov-report=xml + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: coverage.xml +``` + +--- + +## Version update checklist + +When updating tool versions, change them in all three places: + +| Tool | pyproject.toml | .pre-commit-config.yaml | requirements.txt / pyproject deps | +|---|---|---|---| +| ruff | `target-version` (Python, not ruff) | `rev: v0.9.0` | `ruff>=0.9.0` | +| bandit | — | `rev: 1.8.3` | `bandit>=1.8.3` | +| semgrep | — | `rev: v1.60.0` | `semgrep>=1.60.0` | +| basedpyright | `pythonVersion` (Python, not tool) | (local hook — no rev) | `basedpyright>=1.x` | +| pre-commit-hooks | — | `rev: v5.0.0` | — | diff --git a/template/.claude/skills/python-code-quality/references/pre-commit.md b/template/.claude/skills/python-code-quality/references/pre-commit.md new file mode 100644 index 0000000..6eaee4f --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/pre-commit.md @@ -0,0 +1,221 @@ +# pre-commit + +pre-commit is a framework for managing Git hooks. It runs configured tools automatically +before each commit, blocking the commit if any check fails. + +--- + +## What it does + +- Installs hooks into `.git/hooks/` on `pre-commit install` +- On `git commit`, runs each hook against staged files only (fast) +- In CI, run against all files with `pre-commit run --all-files` + +--- + +## Installation + +```bash +pip install pre-commit # or: uv add --dev pre-commit +pre-commit install # installs the git hook — run once per clone +``` + +Verify the hook was installed: +```bash +cat .git/hooks/pre-commit # should reference pre-commit +``` + +--- + +## Complete .pre-commit-config.yaml + +This is the canonical config for all tools in this project. See +`references/complete-configs.md` for a copy you can paste directly. + +```yaml +# Minimum pre-commit version required. +minimum_pre_commit_version: "3.0.0" + +# Default Python version used when a hook doesn't specify one. +default_language_version: + python: python3.11 + +repos: + # ── Ruff: lint + format ────────────────────────────────────────────────── + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 # pin to a specific tag + hooks: + - id: ruff + args: ["--fix"] # auto-fix safe issues on commit + - id: ruff-format # auto-format on commit + + # ── Bandit: security lint ───────────────────────────────────────────────── + - repo: https://github.com/PyCQA/bandit + rev: 1.8.3 + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + # Scope to src/ only — test code legitimately uses assert, subprocess, etc. + files: ^src/ + + # ── Semgrep: pattern-based security scan ───────────────────────────────── + - repo: https://github.com/semgrep/semgrep + rev: v1.60.0 # pin to a specific release + hooks: + - id: semgrep + args: ["--config", ".semgrep.yml", "--error"] + files: ^src/ + # pass_filenames: true is correct for per-file analysis. + # Cross-file taint rules require running semgrep outside pre-commit (in CI). + + # ── basedpyright: type checking ────────────────────────────────────────── + # Runs as a `local` hook so it uses the project's installed venv. + # Requires basedpyright to be installed: pip install basedpyright + - repo: local + hooks: + - id: basedpyright + name: basedpyright + entry: basedpyright + language: system # uses whatever basedpyright is on PATH + types: [python] + pass_filenames: false # basedpyright analyses the whole project graph + + # ── General hygiene ────────────────────────────────────────────────────── + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: debug-statements # catches leftover breakpoint() / pdb.set_trace() +``` + +### Hook ordering rationale + +Hooks run in the order listed. Fast, auto-fixing hooks go first so the developer sees +clean output; slow analysis hooks (basedpyright) go last. + +``` +ruff (fix + format) → bandit → semgrep → basedpyright → hygiene +``` + +--- + +## Running pre-commit + +```bash +# Run all hooks against all files (use after install or config changes): +pre-commit run --all-files + +# Run a specific hook only: +pre-commit run ruff --all-files +pre-commit run bandit --all-files +pre-commit run semgrep --all-files +pre-commit run basedpyright --all-files + +# Run against staged files only (mirrors what git commit does): +pre-commit run + +# Run against a specific file: +pre-commit run --files src/mymodule.py +``` + +--- + +## Skipping hooks + +**Skip all hooks for one commit** (use very rarely — e.g. emergency hotfix): +```bash +git commit --no-verify -m "emergency: revert broken deploy" +``` + +**Skip a specific hook for one commit:** +```bash +SKIP=basedpyright git commit -m "wip: partially typed module" +SKIP=semgrep,bandit git commit -m "temp: adding fixture with insecure pattern" +``` + +**Exclude files permanently** in the hook config: +```yaml +- id: ruff + exclude: "^tests/fixtures/" # regex matched against the file path +``` + +--- + +## Updating hook versions + +Always pin `rev:` to a tagged release. Update intentionally: + +```bash +# Update all hooks to latest tagged release: +pre-commit autoupdate + +# Update a single repo: +pre-commit autoupdate --repo https://github.com/astral-sh/ruff-pre-commit +``` + +After updating, run `pre-commit run --all-files` to confirm nothing broke. + +--- + +## CI integration (GitHub Actions) + +Use the dedicated action — it caches hook environments automatically: + +```yaml +- name: Run pre-commit + uses: pre-commit/action@v3.0.1 +``` + +This caches `~/.cache/pre-commit` keyed on the hash of `.pre-commit-config.yaml`. + +**Important for `language: system` hooks** (basedpyright): the hook runs whatever is on +`PATH`, so project dependencies must be installed *before* pre-commit runs: + +```yaml +- name: Install Python dependencies + run: pip install -r requirements.txt # or: uv sync — must include basedpyright + +- name: Run pre-commit + uses: pre-commit/action@v3.0.1 +``` + +If you run individual tool steps (ruff, basedpyright, etc.) as separate CI jobs, the +pre-commit job is optional but still useful as a final gate — it catches hooks that +aren't individually tested. + +--- + +## Adding a new hook + +1. Find the hook repo at [pre-commit.com/hooks](https://pre-commit.com/hooks.html) or + the tool's own docs. +2. Add a `- repo:` block to `.pre-commit-config.yaml` in the appropriate position + (fast/auto-fixing hooks first, slow analysis hooks last). +3. Pin `rev:` to a tagged release — never `HEAD` or a branch name. +4. Run `pre-commit run --all-files` to validate. +5. Add the tool to the table in `SKILL.md` and create `references/<toolname>.md`. +6. Copy the updated full config to `references/complete-configs.md`. + +--- + +## Gotchas + +- **Staged-only mode can hide issues.** On commit, pre-commit checks staged files only. + Unstaged changes interacting with staged ones can produce a passing local commit that + fails CI (which runs `--all-files`). Always run `pre-commit run --all-files` before + pushing to a shared branch. +- **`language: system` hooks depend on PATH.** The basedpyright hook uses whatever + `basedpyright` is on `PATH`. If your venv isn't activated, it either uses a wrong + version or fails entirely. In CI, install dependencies before running pre-commit. +- **Cache in `~/.cache/pre-commit`.** If a hook behaves strangely after a version bump, + clear it: `pre-commit clean`. Then re-run `pre-commit install`. +- **`pass_filenames: false` is required for whole-project tools.** basedpyright resolves + the full import graph; passing individual filenames breaks cross-module type inference. + Semgrep's per-file mode (`pass_filenames: true`) is fine for single-file rules but + won't catch cross-file taint flows — run those in CI without pre-commit. +- **`rev` must be a tag.** pre-commit warns if you use a branch; tags guarantee + reproducibility across machines and CI runs. diff --git a/template/.claude/skills/python-code-quality/references/ruff.md b/template/.claude/skills/python-code-quality/references/ruff.md new file mode 100644 index 0000000..0723c73 --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/ruff.md @@ -0,0 +1,203 @@ +# Ruff + +Ruff is a fast Python linter and formatter written in Rust. It replaces flake8, isort, +and black in a single tool. + +--- + +## What it does + +- **`ruff check`** — lints: enforces rules (unused imports, style, complexity, bugs, etc.) +- **`ruff format`** — formats: opinionated code formatter (Black-compatible output) + +Both read from `[tool.ruff]` in `pyproject.toml`. + +--- + +## Installation + +```bash +pip install ruff # or: uv add --dev ruff +``` + +Verify: +```bash +ruff --version +``` + +--- + +## pyproject.toml config (annotated) + +```toml +[tool.ruff] +# Target Python version — affects which syntax is valid and which rules apply. +# Keep in sync with the minimum Python version the project supports. +target-version = "py311" + +# Directories ruff will scan. Excludes generated, vendored, or build dirs. +src = ["src"] +exclude = [ + ".git", + ".venv", + "__pycache__", + "build", + "dist", +] + +[tool.ruff.lint] +# Rule sets to enable. Add a prefix letter to enable the entire ruleset. +# E/W = pycodestyle errors/warnings +# F = pyflakes (undefined names, unused imports) +# I = isort (import order) — replaces standalone isort +# UP = pyupgrade (modernise syntax for target Python version) +# B = flake8-bugbear (likely bugs and design issues) +# SIM = flake8-simplify (suggest simpler expressions) +# S = flake8-bandit (security — lightweight overlap with bandit) +select = ["E", "W", "F", "I", "UP", "B", "SIM"] + +# Rules to ignore project-wide. +# E501 = line too long — controlled by the formatter, not the linter. +ignore = ["E501"] + +# Allow autofix for all enabled rules when running `ruff check --fix`. +fixable = ["ALL"] +unfixable = [] + +# Per-file rule overrides. Use sparingly — prefer fixing the root cause. +[tool.ruff.lint.per-file-ignores] +# Test files: assert is valid (pytest), magic values are fine, fixtures look unused. +# S101 requires the S ruleset to be in `select` above — add "S" if you want it. +"tests/**/*.py" = ["PLR2004", "F401"] +# __init__.py: re-exports don't need to be explicitly used. +"**/__init__.py" = ["F401"] + +[tool.ruff.format] +# Double quotes matches Black's default. +quote-style = "double" +# Spaces, not tabs. +indent-style = "space" +# Preserve magic trailing commas (they affect how multi-line structures are formatted). +skip-magic-trailing-comma = false +# Normalise line endings to LF on all platforms. +line-ending = "lf" +# Line length for the formatter. Default is 88 (same as Black). +# If you change this, also set line-length under [tool.ruff.lint] for E501. +line-length = 88 +``` + +--- + +## Common rule codes and fixes + +| Code | Meaning | Fix | +|---|---|---| +| `F401` | Unused import | Remove import; `# noqa: F401` only for intentional re-exports | +| `F811` | Redefinition of unused name | Remove or rename the duplicate | +| `F841` | Local variable assigned but never used | Remove assignment or rename to `_` | +| `E711` | `== None` instead of `is None` | Use `is None` / `is not None` | +| `E712` | `== True` instead of `is True` | Use `is True` or just the boolean directly | +| `I001` | Import block unsorted | Run `ruff check --fix` to auto-sort | +| `UP006` | Use `list` instead of `List` (3.9+) | Update type annotation | +| `UP007` | Use `X \| Y` instead of `Optional[X]` (3.10+) | Update union syntax | +| `B006` | Mutable default argument | Use `None` default + guard in body | +| `B007` | Loop variable unused | Rename to `_` | +| `B008` | Function call as default argument | Move call inside the function body | +| `SIM108` | Ternary can replace if/else | Simplify to `x = a if cond else b` | +| `SIM117` | Nested `with` can be merged | Use `with a(), b():` | + +### Looking up an unfamiliar rule + +```bash +ruff rule F401 # prints full explanation of the rule +ruff rule --all # lists every available rule with description +``` + +### Suppressing a rule inline + +```python +import os # noqa: F401 ← suppress one specific rule (preferred) +import os # noqa ← suppress all rules on this line (avoid) +``` + +Prefer adding an entry to `per-file-ignores` in `pyproject.toml` over scattering +`# noqa` across files — it keeps suppression decisions visible and reviewable. + +--- + +## Running ruff + +```bash +# Check (no changes, exits non-zero on violations): +ruff check . + +# Check and auto-fix safe issues: +ruff check --fix . + +# Show a summary of which rules fired most (useful for tuning config): +ruff check --statistics . + +# Format (rewrites files in place): +ruff format . + +# Format check — CI mode: no changes, exits non-zero if formatting needed: +ruff format --check . + +# Show exactly what the formatter would change without writing: +ruff format --diff . + +# Check a single file: +ruff check src/mymodule.py +``` + +--- + +## CI step (GitHub Actions) + +```yaml +- name: Install ruff + run: pip install ruff + +- name: Ruff — format check + run: ruff format --check . + +- name: Ruff — lint + run: ruff check . +``` + +Always run `ruff format --check` before `ruff check` — formatting failures surface +first without wasting time on lint output. + +With `cache: 'pip'` on `actions/setup-python`, repeated installs are fast. + +--- + +## Pre-commit hook entry + +```yaml +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 # pin to a specific release; update with: pre-commit autoupdate + hooks: + - id: ruff + args: ["--fix"] # auto-fix on commit so the developer sees clean diffs + - id: ruff-format # auto-format on commit +``` + +--- + +## Gotchas + +- **`ruff format` and `ruff check` are separate concerns.** A file can pass linting but + still need formatting, and vice versa. Always run both. +- **`--fix` in local dev, `--check` in CI.** Using `--fix` in CI silently rewrites files; + the pipeline appears to pass but the rewrite never gets committed. Use check mode in CI. +- **`target-version` drives UP rules.** `UP006` (drop `List`/`Dict`) only fires at + `py39+`. Keep `target-version` in sync with your actual minimum Python. +- **isort is built in via the `I` ruleset.** Do not also run standalone isort — they will + conflict on import ordering decisions. +- **`S` ruleset (flake8-bandit) overlaps with bandit.** If you add `"S"` to `select`, + expect duplicate findings. Either add it and accept overlap, or omit it and rely on + bandit for security rules. If you add `"S"`, also update `per-file-ignores` for tests + to include `"S101"` (assert statements). +- **Line length must be set consistently.** If you change `line-length` from 88, set it + in *both* `[tool.ruff.format]` and `[tool.ruff.lint]` (for `E501` if you enable it). diff --git a/template/.claude/skills/python-code-quality/references/semgrep.md b/template/.claude/skills/python-code-quality/references/semgrep.md new file mode 100644 index 0000000..13cafed --- /dev/null +++ b/template/.claude/skills/python-code-quality/references/semgrep.md @@ -0,0 +1,226 @@ +# Semgrep + +Semgrep is a fast, multi-language static analysis tool that matches AST-aware code +patterns. It complements bandit: bandit targets a fixed catalogue of Python security +rules, while semgrep lets you use curated registries and write project-specific custom +rules. + +--- + +## What it does + +- Matches structural code patterns without executing code +- Ships with curated rule registries (`p/python`, `p/owasp-top-ten`, `p/security-audit`) +- Supports custom rules written in YAML — the main value-add over bandit +- Reports findings with severity, rule ID, file, and line number + +--- + +## Installation + +```bash +pip install semgrep # or: uv add --dev semgrep +``` + +The PyPI package bundles the semgrep engine — no separate binary needed. + +Verify: +```bash +semgrep --version +``` + +--- + +## Configuration + +Semgrep does **not** read `pyproject.toml`. All configuration lives in `.semgrep.yml` +(or a `.semgrep/` directory) at the project root and is passed via `--config`. + +--- + +## .semgrep.yml (annotated) + +```yaml +# .semgrep.yml — rule configuration file at the project root. +# +# Registry packs (p/python etc.) cannot be embedded directly in this file — +# they must be passed via --config on the CLI or in pre-commit args. +# This file is for LOCAL custom rules only. +# +# To use registry packs alongside local rules, pass multiple --config flags: +# semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/ + +rules: + # ── Example custom rule ────────────────────────────────────────────────── + # Custom rules give semgrep its real advantage over bandit. + # Save additional rules in .semgrep/rules/<name>.yml and reference them + # with: semgrep --config .semgrep/rules/ src/ + + - id: no-hardcoded-jwt-secret + patterns: + - pattern: jwt.encode(..., "$SECRET", ...) + - pattern-not: jwt.encode(..., os.environ.get(...), ...) + message: > + Hardcoded JWT secret. Load the secret from an environment variable + or secrets manager instead. + languages: [python] + severity: ERROR + + - id: no-eval + pattern: eval(...) + message: > + eval() executes arbitrary code. Use ast.literal_eval() for safe + data parsing, or refactor to avoid dynamic evaluation. + languages: [python] + severity: ERROR + + - id: no-print-statements + pattern: print(...) + message: Use the logging module instead of print() in production code. + languages: [python] + severity: WARNING + # Only enforce this in src/, not tests — pass `files: ^src/` in pre-commit. +``` + +### Structuring custom rules + +For more than a handful of rules, organise under `.semgrep/rules/`: + +``` +.semgrep/ +├── rules/ +│ ├── security.yml # project-specific security rules +│ ├── style.yml # project-specific style rules +│ └── tests/ # test fixtures for rules (semgrep --test) +│ └── security_test.py +``` + +Reference the whole directory: `semgrep --config .semgrep/rules/ src/` + +--- + +## Common registry rule IDs and fixes + +These are available via `--config p/python` or `--config p/security-audit`: + +| Rule ID | Issue | Fix | +|---|---|---| +| `python.lang.security.audit.exec-detected` | `exec()` call | Refactor to explicit function calls | +| `python.lang.security.audit.eval-detected` | `eval()` call | Use `ast.literal_eval()` for data | +| `python.lang.security.audit.dangerous-system-call` | `os.system()` | Use `subprocess.run(["cmd"], check=True)` | +| `python.lang.security.audit.formatted-sql-query` | SQL via f-string / `%` | Use parameterised queries | +| `python.lang.security.audit.jinja2.autoescape-disabled` | XSS via Jinja2 | Set `autoescape=True` | +| `python.django.security.audit.raw-query` | Django raw SQL | Use the ORM or parameterised `raw()` | +| `python.requests.security.no-auth-over-http` | Credentials over HTTP | Enforce HTTPS | +| `python.lang.security.insecure-hash-use` | MD5 / SHA1 for security | Use SHA-256 or better | + +--- + +## Suppressing a finding + +**Inline (single line):** +```python +result = eval(user_input) # nosemgrep: python.lang.security.audit.eval-detected +``` + +**Whole file** (add at top of file): +```python +# nosemgrep: python.lang.security.audit.eval-detected +``` + +Always include the specific rule ID. A bare `# nosemgrep` suppresses all findings on +the line and makes the intent invisible to reviewers. + +--- + +## Running semgrep + +```bash +# Run local rules in .semgrep.yml: +semgrep --config .semgrep.yml src/ + +# Run a registry pack (fetches rules from semgrep.dev on first run, then caches): +semgrep --config p/python src/ + +# Run multiple sources together: +semgrep --config p/python --config p/owasp-top-ten --config .semgrep.yml src/ + +# Restrict to ERROR severity only (suppress WARNING / INFO): +semgrep --config p/python --severity ERROR src/ + +# JSON output for CI artefacts or downstream tooling: +semgrep --config .semgrep.yml --json src/ -o semgrep-report.json + +# Test custom rules against fixtures in .semgrep/rules/tests/: +semgrep --test .semgrep/rules/ +``` + +Exit codes: `0` = no findings, `1` = findings found. + +--- + +## CI step (GitHub Actions) + +```yaml +- name: Install semgrep + run: pip install semgrep + +# Cache registry rules to avoid re-fetching on every run. +- name: Cache semgrep rules + uses: actions/cache@v4 + with: + path: ~/.semgrep/cache + key: semgrep-${{ hashFiles('.semgrep.yml') }} + +# Run registry packs + local rules together. +- name: Semgrep scan + run: | + semgrep --config p/python \ + --config p/owasp-top-ten \ + --config .semgrep.yml \ + --severity ERROR \ + src/ +``` + +See `SKILL.md` for the full CI ordering across all tools. + +--- + +## Pre-commit hook entry + +```yaml +- repo: https://github.com/semgrep/semgrep + rev: v1.60.0 # pin to a specific release; update with: pre-commit autoupdate + hooks: + - id: semgrep + # Registry packs can be passed here too. + args: ["--config", "p/python", "--config", ".semgrep.yml", "--severity", "ERROR"] + # Scope to src/ only. + files: ^src/ + # pass_filenames: true is correct for per-file pattern matching. + # Cross-file taint analysis is not supported in pre-commit mode. +``` + +--- + +## Gotchas + +- **Registry packs are CLI flags, not YAML rule entries.** You cannot write `- p/python` + inside a `rules:` block in `.semgrep.yml` — that syntax is invalid. Pass registries + with `--config p/python` on the command line or in the pre-commit `args`. +- **Registry rules require internet access on first run.** Rules are cached in + `~/.semgrep/cache`. In an air-gapped CI environment, vendor the rules locally by + downloading them and adding as local rule files. +- **`p/security-audit` is noisy.** It includes WARNING and INFO findings on top of + ERROR. Use `--severity ERROR` in CI until the baseline is clean, then broaden. +- **Semgrep is slower than bandit on first run** because it fetches and compiles registry + rules. Cache `~/.semgrep/cache` in CI to keep subsequent runs fast. +- **Custom rules are the real value-add.** Registry rules catch generic issues; custom + rules catch your project's specific anti-patterns. Even two or three custom rules + tailored to your stack are worth writing. +- **`--config auto` changes silently over time.** Auto mode infers rulesets from your + codebase and will shift as semgrep's auto-detection improves. Use explicit `--config` + flags in CI for reproducible results. +- **Overlap with bandit is intentional.** Both tools flag `eval`, insecure hashes, and + SQL injection but via different matching mechanisms. The overlap means issues are + caught by at least one tool even if the other misses a variant. diff --git a/template/.claude/skills/python-code-reviewer/SKILL.md b/template/.claude/skills/python-code-reviewer/SKILL.md new file mode 100644 index 0000000..fddb85c --- /dev/null +++ b/template/.claude/skills/python-code-reviewer/SKILL.md @@ -0,0 +1,204 @@ +--- +name: python-code-reviewer +description: >- + Performs structured, multi-phase Python code review covering security, correctness, + Pythonic style, type safety, performance, tests, and documentation. Targets Python 3.11+. + ALWAYS use this skill when Python code appears anywhere in the conversation — even if the + user has not explicitly asked for a review. Trigger on: "review", "check", "audit", + "critique", "look at this", "any issues?", "is this good?", "thoughts?", "feedback", + "what do you think?", or whenever a Python snippet, file, class, function, or PR diff + is pasted. Do NOT rely on memory for review patterns — always load this skill. + Covers Django, Flask, FastAPI, async, data science (pandas/numpy/torch), and general Python. +--- + +# Python Code Reviewer Skill + +You are a senior Python code reviewer. Every finding must be **confident** (>80% sure it is +a real issue), **specific** (cite file + line when possible), and **fixable** (include a +concrete corrected snippet). Never flood the review with noise — consolidate similar issues. + +**Assumed Python version: 3.11** unless overridden by `python_requires` in `pyproject.toml` +or `setup.cfg`. Note version-conditional advice where relevant. + +--- + +## Phase 0 — Gather Context + +Determine before reviewing: + +1. **Scope** — snippet / single function / module / PR diff / full repo? +2. **Purpose** — what does this code do? Read docstrings, imports, READMEs. +3. **Python version** — check `pyproject.toml`, `setup.cfg`, `.python-version`. Default: 3.11. +4. **Conventions in use** — test framework, formatter, existing style, annotation density. +5. **What NOT to flag** — do not report issues in *unchanged* code unless CRITICAL security. + +**Decision gate — when context is insufficient:** +- Code **< 30 lines**: proceed directly; state assumptions in the review header. +- Code **≥ 30 lines** with unclear purpose and no context: ask **one** clarifying question + (e.g. "Is this a web handler or a background job?"). Make assumptions for everything else. +- **Never ask more than one question** per iteration. + +--- + +## Phase 1 — Automated Tools + +**If a bash tool or terminal is available**, run these and cite exact output in findings: + + mypy --strict . + ruff check . + black --check . + bandit -r . -ll + pytest --tb=short --cov=. --cov-report=term-missing + +Surface any ERROR / WARNING lines as findings at the appropriate severity. + +**If no tools are available**: skip Phase 1 entirely. Add to the review header: + ⚠️ Automated tools not run — manual review only. + +Do NOT write "would fail mypy" as mock output. If you cannot run the tool, flag the issue +from first principles in Phase 2 with a specific line citation instead. + +--- + +## Phase 2 — Structured Review + +### Review Mode + +Select before starting: + +| Mode | When to use | Categories | Max findings | +|------|-------------|------------|--------------| +| **Quick** | < 30 lines, or user says "quick look" / "just check X" | Security + Correctness only | 3 | +| **Standard** | Default | All 8 categories below | Unlimited | +| **Deep** | Full module, audit, "thorough review" | All 13 in `checklist.md` | Unlimited | + +For **Deep** mode: load `references/checklist.md` now and work through all 13 sections. + +--- + +### Severity Definitions + +| Severity | Meaning | Block merge? | +|----------|---------|--------------| +| CRITICAL | Security vuln, data loss, auth bypass, hardcoded secret | Yes — always | +| HIGH | Bug in normal use path; missing I/O error handling | Yes | +| MEDIUM | Maintenance debt, test gap, type error, bad pattern | Recommended | +| LOW | Naming, minor style, optional improvement | No (nit) | + +Report CRITICAL issues at the top — never bury them. + +--- + +### Category Prompts (Standard mode) + +These are decision prompts — they tell you *whether* to flag an issue in each area. +For the sub-items, load `references/checklist.md`. Do not duplicate checklist content here. + +**1. Security** (always check, all modes) +- Hardcoded credentials / tokens? SQL string concatenation? `eval()`/`exec()` on input? +- Unsafe deserialization (`pickle`, `yaml.load` without SafeLoader)? +- Path traversal? Weak crypto (`MD5`/`SHA1`)? Insecure `random` for tokens? JWT misconfig? +- See `references/python-patterns.md` Section 1 for before/after fixes. + +**2. Correctness & Bugs** +- Mutable defaults? Late-binding closures? Bare `except`? Missing context managers? +- Mutating a collection while iterating? Integer vs float division? + +**3. Type Safety** (Python 3.11 idioms) +- Use `X | Y` unions (not `Optional[X]` or `Union[X, Y]`). +- Use `Self` for methods returning the same class. Use `Never` for functions that always raise. +- Missing annotations on public APIs? `Any` overused? `# type: ignore` without comment? +- `TypeVarTuple` / `Unpack` for variadic generics where applicable. + +**4. Pythonic Style & PEP 8** +- `is`/`is not` for None? `match`/`case` for structural dispatch over long `if`/`elif` chains? +- f-strings over `.format()`? `pathlib.Path` over `os.path`? `enumerate` over `range(len)`? +- `tomllib` (stdlib in 3.11) instead of third-party toml packages? + +**5. Design & Architecture** +- Single responsibility? Functions > 30 lines? Deep nesting (> 3 levels)? +- Boolean flag parameters that switch behaviour — consider two functions instead. +- Magic numbers as named constants? Hard-coded config mixed with logic? + +**6. Error Handling** +- `except Exception: pass` swallowing errors? Exception chain lost? + (`raise X` loses original; use `raise X from e`) +- Resource leaks not covered by a `with` block? +- `ExceptionGroup` / `except*` available in 3.11 — use for concurrent exception handling. + +**7. Tests** +- New logic without tests? Tests asserting real behaviour, not just "no exception"? +- Edge cases: empty input, None, zero, boundary values? +- Mock targets at the right import path? No real I/O in unit tests? + +**8. Documentation** +- Public functions/classes missing docstrings? +- Comments explain *why*, not just *what*? +- README / CHANGELOG stale after interface change? + +--- + +## Phase 3 — Output the Review + +Load `references/output-format.md` and select the matching template: +- **Template A** — snippet / function / module (most common) +- **Template B** — PR / diff review +- **Template C** — full module audit (Deep mode only) + +Always include at least one "What's Good" item — even for heavily flawed code. + +--- + +## Phase 4 — Calibrate Feedback Tone + +- **Consolidate**: "5 functions missing type annotations" not 5 separate items. +- **Label nits**: prefix LOW items with `[Nit]` so the author knows they are optional. +- **Praise specifically**: name the exact pattern or decision done well. +- **Don't police formatting** that `black`/`ruff` auto-fix. +- **Design opinions → questions**: "Could this be split into two functions?" not "Split this." +- **Never** tie findings to developer skill. Critique the code, not the author. + +--- + +## Phase 5 — Self-Check (run on draft before sending) + +- [ ] Every CRITICAL/HIGH finding has a specific line citation and a concrete fix snippet +- [ ] Similar issues are consolidated, not listed individually +- [ ] At least one "What's Good" item is present +- [ ] Verdict is consistent with the findings (no CRITICAL → APPROVE is wrong) +- [ ] No formatter-fixable style issues are reported as findings +- [ ] Tone is constructive throughout — no statements about the author's ability + +--- + +## Phase 6 — Follow-Up / Iterative Reviews + +When the author submits a revision: + +1. **Incremental scope** — only look at changed lines. Do not re-raise already-fixed issues. + Do not introduce new findings on unchanged code (except CRITICAL security). +2. **Author disagreement** — acknowledge their argument. Update verdict if technically valid. + If not valid, explain once clearly and do not repeat the point in subsequent rounds. +3. **Closing out** — once all CRITICAL and HIGH issues are resolved, approve. + Open MEDIUMs and LOWs are not a blocking reason — note them as follow-up candidates. +4. **Post-merge** — treat as a standard review; open issues for CRITICAL/HIGH rather than blocking. + +--- + +## Quick reference: where to go deeper + +| Topic | Reference file | +|------------------------------------|--------------------------------------------------------------------------| +| Full 13-category review checklist | [references/checklist.md](references/checklist.md) | +| Before/after code pattern fixes | [references/python-patterns.md](references/python-patterns.md) | +| Output format templates (A/B/C) | [references/output-format.md](references/output-format.md) | + +--- + +## Noise Filters (always apply) + +- Do NOT report: unused imports, trailing whitespace, import order — ruff/black handles these +- Do NOT report: line-length violations if the file has a non-default `line-length` config +- Do NOT report: style opinions not in PEP 8 or the project's established conventions +- Do NOT report: issues in third-party code, `migrations/`, or generated files +- DO report even in unchanged code: any CRITICAL security vulnerability noticed in passing diff --git a/template/.claude/skills/python-code-reviewer/references/checklist.md b/template/.claude/skills/python-code-reviewer/references/checklist.md new file mode 100644 index 0000000..c0afc87 --- /dev/null +++ b/template/.claude/skills/python-code-reviewer/references/checklist.md @@ -0,0 +1,291 @@ +# Review checklist + +> Load this file for Deep mode reviews, thorough audits, or when uncertain about +> specific sub-checks in any category. All checklist items assume Python 3.11 +> unless marked with a version note. + +--- + +## Table of Contents +1. Security +2. Correctness & Bugs +3. Type Safety +4. Pythonic Style & PEP 8 +5. Design & Architecture +6. Error Handling +7. Testing +8. Performance +9. Concurrency & Async +10. Dependencies +11. Documentation +12. Framework Specifics (Django / Flask / FastAPI) +13. Data Science (Pandas / NumPy / PyTorch) + +--- + +## 1. Security + +### Injection +- [ ] SQL built with string formatting or concatenation instead of parameterised queries? +- [ ] `subprocess` called with `shell=True` and user-controlled input? +- [ ] Template injection: `render_template_string` or `jinja2.Template(user_input)`? +- [ ] `eval()`, `exec()`, or `compile()` called on user-controlled strings? + +### Deserialization +- [ ] `pickle.loads()` on untrusted data? (Use JSON or sign the payload) +- [ ] `yaml.load()` without `Loader=yaml.SafeLoader`? +- [ ] `marshal`, `shelve`, or `__reduce__` used with untrusted data? + +### Path Traversal +- [ ] User-controlled path not validated with `os.path.normpath()` + prefix check? +- [ ] `open(user_input)` without sanitisation? +- [ ] `send_file(user_input)` in Flask/FastAPI without `safe_join`? + +### Secrets & Auth +- [ ] API keys, passwords, or tokens hardcoded in source? +- [ ] Secrets in committed config files instead of `.env` or a secrets manager? +- [ ] `MD5` or `SHA1` used for security (passwords, tokens) — not just checksums? +- [ ] `random` module used for security tokens? (Use `secrets` module) +- [ ] JWT decoded with `algorithm=None` or `options={"verify_signature": False}`? +- [ ] `hashlib.pbkdf2_hmac` with too few iterations (< 600 000 for PBKDF2-SHA256)? + +### Web Security +- [ ] CSRF protection missing on state-changing endpoints? +- [ ] Cookies missing `HttpOnly` / `Secure` / `SameSite` flags? +- [ ] User input rendered in HTML without escaping? +- [ ] CORS configured as `allow_origins=["*"]` with `allow_credentials=True`? +- [ ] Rate limiting absent on authentication endpoints? + +--- + +## 2. Correctness & Bugs + +### Python-Specific Traps +- [ ] Mutable default argument: `def f(items=[])` — use `None` + guard +- [ ] Late-binding closure: `lambda: x` inside a loop captures loop variable by reference +- [ ] Integer vs float: `5 / 2 == 2.5`; use `//` if integer result intended +- [ ] `is` vs `==`: `is` for identity (None, True, False only); `==` for value equality +- [ ] String concatenation in a loop (`s += chunk`) — O(n²); use `"".join()` +- [ ] `dict.keys()` iterated while the dict is mutated +- [ ] `datetime.datetime.utcnow()` deprecated in 3.12 — prefer `datetime.now(UTC)` now + +### Logic +- [ ] Off-by-one errors in slices (`[:n]` vs `[:n+1]`) or `range()` +- [ ] Incorrect boolean short-circuit assumptions +- [ ] Function that should return a value has a path that falls through to implicit `None` +- [ ] Unreachable code after `return` / `raise` / `continue` / `break` + +### Data Structures +- [ ] List used for O(n) membership tests that should be a `set` (O(1)) +- [ ] Modifying a list or dict while iterating it +- [ ] Shallow copy (`list.copy()`, `dict.copy()`) where deep copy is needed +- [ ] `collections.defaultdict` or `Counter` would simplify the pattern + +--- + +## 3. Type Safety (Python 3.11) + +- [ ] Use `X | Y` union syntax (3.10+) — not `Union[X, Y]` or `Optional[X]` +- [ ] Use `Self` (from `typing`) for methods that return the instance type +- [ ] Use `Never` for functions that always raise or never return +- [ ] Use `TypeVarTuple` / `Unpack` for variadic generics +- [ ] `LiteralString` used for SQL/shell strings built from user input (3.11) +- [ ] Public functions missing parameter and return type annotations +- [ ] `Any` used where a more specific type is knowable +- [ ] `Optional` result used at callsite without a `None` check +- [ ] `cast()` used to silence mypy rather than fix a real type inconsistency +- [ ] `# type: ignore` without an explanatory comment — is it justified? +- [ ] `isinstance()` used for runtime checks (correct); `type(x) ==` avoided +- [ ] `dataclass`, `TypedDict`, or Pydantic `BaseModel` used for structured data +- [ ] `@overload` used where the return type depends on argument type + +--- + +## 4. Pythonic Style & PEP 8 + +### Naming +- [ ] `snake_case` for functions and variables +- [ ] `PascalCase` for classes +- [ ] `UPPER_CASE` for module-level constants +- [ ] No single-letter names outside comprehensions, lambdas, or mathematical code +- [ ] No `l`, `O`, or `I` as variable names (ambiguous with `1`, `0`, `1`) + +### Modern Python 3.11 Idioms +- [ ] `match`/`case` used for structural dispatch instead of long `if`/`elif` chains (3.10+) +- [ ] `tomllib` (stdlib 3.11) instead of third-party `toml` / `tomli` packages +- [ ] `datetime.now(timezone.utc)` instead of deprecated `utcnow()` +- [ ] `StrEnum` (3.11) for string-valued enums instead of `(str, Enum)` +- [ ] `ExceptionGroup` and `except*` for concurrent/multi-error handling (3.11) +- [ ] `typing.assert_never()` in exhaustive `match` branches to catch missing cases + +### General Idioms +- [ ] `if x is None:` not `if x == None:` +- [ ] `if not items:` not `if len(items) == 0:` +- [ ] f-strings for interpolation (not `.format()` or `%`) +- [ ] `with open(...) as f:` — not manual `f.open()` / `f.close()` +- [ ] `enumerate()` instead of `range(len(...))` +- [ ] `zip()` to iterate parallel sequences; `zip(..., strict=True)` (3.10+) to catch length mismatches +- [ ] `pathlib.Path` over `os.path` string manipulation +- [ ] `_` for unused loop variables: `for _ in range(n):` +- [ ] `dict | other` for merging dicts (3.9+) over `{**a, **b}` + +### Code Style +- [ ] 2 blank lines around top-level definitions; 1 blank line between methods +- [ ] Imports: stdlib → third-party → local; one import per line +- [ ] No wildcard imports (`from module import *`) except deliberate `__init__.py` re-exports +- [ ] No semicolons combining multiple statements on one line + +--- + +## 5. Design & Architecture + +### Functions +- [ ] Single responsibility — does each function do exactly one thing? +- [ ] Functions > 30 lines — consider splitting +- [ ] > 3–4 parameters without a dataclass/config object grouping them +- [ ] Boolean flag parameter that switches behaviour — consider two separate functions +- [ ] Deep nesting (> 3 levels) — can guard clauses / early returns flatten this? +- [ ] Side effects (I/O, mutation) mixed with pure computation in the same function + +### Classes +- [ ] Is a class actually needed, or would module-level functions suffice? +- [ ] `__init__` doing expensive work (I/O, network) — consider a factory `@classmethod` +- [ ] Missing `__repr__` on data-holding classes +- [ ] Inheritance used where composition would be simpler and more explicit +- [ ] `@dataclass` / `NamedTuple` / Pydantic model for data-only classes +- [ ] `__slots__` considered for high-volume small objects + +### Modules +- [ ] Circular imports (A imports B imports A)? +- [ ] Magic numbers that should be named constants +- [ ] Configuration logic mixed with business logic — separate them +- [ ] God module (> 500 lines doing several unrelated things)? +- [ ] Missing `__all__` on a public module (controls what `import *` exposes) + +--- + +## 6. Error Handling + +- [ ] Bare `except:` catches `KeyboardInterrupt`, `SystemExit` — always name the exception +- [ ] `except Exception: pass` silently swallowing errors +- [ ] Original exception lost: `raise RuntimeError(...)` inside except — use `raise X from e` +- [ ] `try` block too wide — wrapping code that cannot realistically raise the caught exception +- [ ] Resource leak: `open()`, DB connection, socket not in a `with` block +- [ ] Custom exceptions inheriting from a semantically appropriate base + (`ValueError`, `RuntimeError`, `OSError`, etc.) +- [ ] `logger.exception()` used (not `logger.error()`) to preserve traceback in except blocks +- [ ] `ExceptionGroup` used where multiple concurrent errors can occur together (3.11) +- [ ] `except*` used to handle specific exception types within an `ExceptionGroup` (3.11) + +--- + +## 7. Testing + +- [ ] New public functions / classes covered by at least one test? +- [ ] Edge cases: empty input, `None`, zero, negative, max/min values, empty collections +- [ ] Tests assert behaviour (not just "no exception raised") +- [ ] Mock / patch targets the right import path: + `mymodule.requests.get`, not `requests.get` +- [ ] No real I/O (filesystem, network, DB) in unit tests — mock or use fixtures +- [ ] Tests are isolated: no shared mutable state, no execution-order dependency +- [ ] `@pytest.mark.parametrize` used for repeated cases instead of copy-paste tests +- [ ] Fixtures reused via `conftest.py` instead of duplicated `setUp`-style code +- [ ] Line coverage ≠ branch coverage ≠ behaviour coverage — check what is actually asserted +- [ ] `pytest-asyncio` used correctly for async tests + +--- + +## 8. Performance + +> Flag only for hot paths, loops over large data, or when explicitly requested. + +- [ ] N+1 query: related objects fetched inside a loop — use `select_related` / `prefetch_related` +- [ ] Expensive value re-computed inside a loop that could be hoisted before the loop +- [ ] String concatenation in a loop — use `"".join()` +- [ ] Reading an entire large file into memory — consider streaming / chunking +- [ ] `for i in range(len(items)):` — use `enumerate` or iterate directly +- [ ] Unnecessary object creation in a tight inner loop + +--- + +## 9. Concurrency & Async + +- [ ] Blocking I/O (`requests.get`, `time.sleep`) inside `async def` — use async alternatives +- [ ] `asyncio.create_task()` result not stored — uncaught exceptions silently discarded +- [ ] `asyncio.sleep(0)` missing to yield control in CPU-bound `async` loops +- [ ] Shared mutable state accessed from multiple threads without a lock +- [ ] `threading.Thread` missing `daemon=True` or `join()` — may hang on exit +- [ ] Race condition in `if not exists: create` — use atomic DB operations or a lock +- [ ] `asyncio.TaskGroup` (3.11) preferred over `asyncio.gather()` for structured concurrency +- [ ] `timeout` parameter on `asyncio.wait_for()` to prevent hung tasks + +--- + +## 10. Dependencies + +- [ ] New dependency not added to `pyproject.toml` / `requirements.txt`? +- [ ] Production deps pinned to an exact version or a narrow range? +- [ ] Known vulnerability in the pinned version (check PyPI Security Advisories)? +- [ ] Heavy library imported for a trivial use that stdlib could handle? +- [ ] `tomllib` (stdlib 3.11) replaces the need for `tomli` or `toml` packages + +--- + +## 11. Documentation + +- [ ] Public functions and classes have docstrings? +- [ ] Docstring style consistent with the project (Google / NumPy / reStructuredText)? +- [ ] Non-obvious logic has a comment explaining *why*, not just *what* +- [ ] Module-level docstring states the module's purpose? +- [ ] TODOs tracked in an issue, not left indefinitely in code +- [ ] `CHANGELOG.md` / `README.md` updated for user-visible interface changes? +- [ ] Deprecation warnings (`warnings.warn(..., DeprecationWarning)`) added for removed APIs? + +--- + +## 12. Framework Specifics + +### Django +- [ ] Raw SQL in `cursor.execute()` using string formatting? +- [ ] `request.POST` / `request.GET` used without form or serializer validation? +- [ ] Views missing `@login_required` or `permission_classes`? +- [ ] Queryset accesses related object in a loop without `select_related`/`prefetch_related`? +- [ ] `null=True` on a `CharField` — use `blank=True`; empty string is the Django convention +- [ ] `SECRET_KEY` or `DEBUG=True` committed to source? +- [ ] Django signals used where an explicit service-layer call would be clearer? + +### Flask +- [ ] `app.secret_key` hardcoded in source? +- [ ] `render_template_string` called with unsanitised user input? +- [ ] `DEBUG=True` in a production config file? +- [ ] Missing `abort(403)` / `abort(404)` where appropriate? + +### FastAPI +- [ ] Request/response bodies use Pydantic `BaseModel`, not raw `dict`? +- [ ] `response_model` specified on all endpoints? +- [ ] Blocking code inside `async def` endpoints? + (Use `def` route handlers or `run_in_executor` for sync I/O) +- [ ] Dependencies injected via `Depends()`, not via global state? +- [ ] `Annotated[X, Depends(...)]` pattern (modern FastAPI style) used? + +--- + +## 13. Data Science (Pandas / NumPy / PyTorch) + +### Pandas +- [ ] Chained indexing `df[col][row]` — use `df.loc[row, col]` +- [ ] `.iterrows()` for something that can be vectorised? +- [ ] `inplace=True` — prefer explicit reassignment (`df = df.dropna()`) +- [ ] Reading a large CSV fully into memory — consider chunking or Polars + +### NumPy +- [ ] Python loop where vectorised NumPy operations would work? +- [ ] Unnecessary copy (`np.copy()`) where a view suffices? +- [ ] Broadcasting assumptions not documented with a comment? + +### PyTorch +- [ ] `model.eval()` and `torch.no_grad()` missing during inference? +- [ ] Tensors not moved to the same device before operations? +- [ ] No random seed set for reproducibility (`torch.manual_seed`, `numpy.random.seed`)? +- [ ] `print()` debugging statements left in notebook or training script? +- [ ] Data loading/preprocessing logic mixed with model logic — consider a `Dataset` class? diff --git a/template/.claude/skills/python-code-reviewer/references/output-format.md b/template/.claude/skills/python-code-reviewer/references/output-format.md new file mode 100644 index 0000000..142d835 --- /dev/null +++ b/template/.claude/skills/python-code-reviewer/references/output-format.md @@ -0,0 +1,270 @@ +# Output format templates + +> Always load this file in Phase 3. Choose the template that matches the review scope. +> Templates are shown as live Markdown — copy and fill in the placeholders. + +--- + +## Verdict Decision Guide (read before selecting template) + +| Situation | Verdict | +|-----------|---------| +| No CRITICAL or HIGH issues | **APPROVE** | +| 1–2 HIGH issues, no CRITICAL | **REQUEST CHANGES** | +| Any CRITICAL issue | **BLOCK** | +| New logic has no tests and project requires them | **REQUEST CHANGES** | +| Only LOW / nit issues remain | **APPROVE** (list nits as optional) | +| Hotfix under time pressure, 1 known HIGH issue | **APPROVE** + open follow-up ticket | + +--- + +## Template A — Snippet / Function / Module Review + +Use for: a pasted function, a single file, or a module-level review. + +--- + +## Python Code Review + +**Mode**: Quick / Standard / Deep +**Python version**: 3.11 +**Tools run**: mypy, ruff, black, bandit, pytest — OR — Manual review only (tools not available) + +> *Assumptions (fill in if context was absent):* +> e.g. "Treating this as a utility module, not a web handler." + +--- + +### Summary + +| Severity | Count | Status | +|----------|-------|--------| +| CRITICAL | N | Block | +| HIGH | N | Warn | +| MEDIUM | N | Info | +| LOW | N | Nit | + +**Verdict**: APPROVE / REQUEST CHANGES / BLOCK + +--- + +### Findings + +#### [CRITICAL] Title of the issue + +**Location**: `path/to/file.py:42` +**Issue**: Clear description of the problem and why it matters (what can go wrong). + +**Before:** + + # The problematic code + eval(user_input) + +**After:** + + # The corrected code + import ast + ast.literal_eval(user_input) + +--- + +#### [HIGH] Title + +**Location**: `path/to/file.py:17` +**Issue**: Description. + +**Fix**: Description of change, plus a snippet if helpful. + + # corrected snippet here + +--- + +#### [MEDIUM] Title + +**Location**: `path/to/file.py:88` +**Issue**: Description. + +**Fix**: Description or snippet. + +--- + +#### [Nit] Title + +**Location**: `path/to/file.py:5` +**Note**: Optional — not required to fix. e.g. "Could rename `d` to `user_data` for clarity." + +--- + +### What's Working Well + +- **[Specific pattern]**: Brief specific praise. Name the exact decision done well. +- **[Another item]**: ... + +--- + +### Automated Tool Results + +| Tool | Status | Notes | +|------|--------|-------| +| `mypy --strict` | Pass / Fail / Not run | ... | +| `ruff check` | Pass / Fail / Not run | ... | +| `black --check` | Pass / Fail / Not run | ... | +| `bandit -r -ll` | Pass / Fail / Not run | ... | +| `pytest --cov` | Pass / Fail / Not run | ... | + +--- + +## Template B — PR / Diff Review + +Use for: a GitHub/GitLab PR, a git diff, or a branch comparison. + +--- + +## PR Review: #NUMBER — TITLE + +**Reviewed**: DATE +**Author**: AUTHOR +**Branch**: `head-branch` → `base-branch` +**Scope**: +N / -N lines across N files +**Tools run**: mypy, ruff, black, bandit, pytest — OR — Manual only + +--- + +### Decision: APPROVE / REQUEST CHANGES / BLOCK + +**Summary**: One sentence verdict. +e.g. "Two HIGH issues in error handling should be resolved before merge." + +--- + +### Summary Table + +| Severity | Count | Status | +|----------|-------|--------| +| CRITICAL | 0 | Pass | +| HIGH | 2 | Warn | +| MEDIUM | 3 | Info | +| LOW | 1 | Nit | + +--- + +### Findings + +(Use the same finding format as Template A — Location / Issue / Before+After) + +--- + +### Validation Results + +| Check | Result | Notes | +|-------|--------|-------| +| `mypy --strict` | Pass / Fail / Skipped | ... | +| `ruff check` | Pass / Fail / Skipped | ... | +| `black --check` | Pass / Fail / Skipped | ... | +| `bandit -r -ll` | Pass / Fail / Skipped | ... | +| Tests | Pass / Fail / Skipped | coverage: N% | + +--- + +### Files Reviewed + +| File | Change | Notes | +|------|--------|-------| +| `src/auth.py` | Modified | Main logic change | +| `tests/test_auth.py` | Added | New tests | +| `requirements.txt` | Modified | New dep added | + +--- + +### What's Working Well + +... + +--- + +### Discussion Points (optional) + +Design observations that do not block merge but are worth discussing async: + +- ... + +--- + +## Template C — Module / Codebase Audit + +Use for: Deep mode reviews of an entire module or unfamiliar codebase. + +--- + +## Python Module Audit: `module_name` + +**Date**: DATE +**Scope**: N files, ~N lines of code +**Python version**: 3.11 +**Purpose**: What this module does + +--- + +### Executive Summary + +2–3 sentences: overall health, most critical areas, and the top recommendation. + +--- + +### Risk Matrix + +| Category | Risk | Key Issues | +|----------|------|------------| +| Security | High / Medium / Low | ... | +| Correctness | ... | ... | +| Test Coverage | ... | ... | +| Type Safety | ... | ... | +| Documentation | ... | ... | +| Performance | ... | ... | + +--- + +### Critical Findings (must fix immediately) + +... + +### High Priority + +... + +### Recommended Improvements (medium / low — prioritised) + +1. ... +2. ... + +--- + +### Strengths + +... + +### Suggested Next Steps + +1. Run `bandit -r . -ll` and address all HIGH findings +2. Enable `mypy --strict` incrementally, module by module +3. ... + +--- + +## Tone Guidelines + +**Use:** +- "This could be simplified by..." +- "Consider using X here — it makes the intent clearer." +- "Nice use of dataclasses for the config object — exactly the right pattern." +- "[Nit] Could rename `d` to `user_data` for clarity." +- "Would it make sense to extract this into a helper to separate concerns?" + +**Avoid:** +- "This is wrong / bad / amateur." +- "You should know not to do this." +- "Obviously this needs to be..." +- Any language that judges the author rather than the code. + +**For optional suggestions**: prefix with `[Nit]` or `[Suggestion]`. +**For design questions**: phrase as open questions, not commands. diff --git a/template/.claude/skills/python-code-reviewer/references/python-patterns.md b/template/.claude/skills/python-code-reviewer/references/python-patterns.md new file mode 100644 index 0000000..7de31cd --- /dev/null +++ b/template/.claude/skills/python-code-reviewer/references/python-patterns.md @@ -0,0 +1,502 @@ +# Python patterns + +> Load this file when you need concrete before/after code examples to include +> in a finding's Fix section. All examples target Python 3.11 unless noted. + +--- + +## Table of Contents +1. Security Anti-Patterns +2. Common Bug Patterns +3. Pythonic Idioms (3.11) +4. Type Annotation Patterns (3.11) +5. Error Handling Patterns +6. Testing Patterns +7. Async Patterns +8. Python 3.11 New Patterns + +--- + +## 1. Security Anti-Patterns + +### SQL Injection + # CRITICAL + query = f"SELECT * FROM users WHERE name = '{username}'" + cursor.execute(query) + + # Fix — parameterised query + cursor.execute("SELECT * FROM users WHERE name = %s", (username,)) + # ORM (Django) — always safe + User.objects.filter(name=username) + +### Unsafe YAML Loading + # CRITICAL + import yaml + data = yaml.load(user_input) + + # Fix + data = yaml.safe_load(user_input) + +### eval on User Input + # CRITICAL + result = eval(user_expression) + + # Fix — safe literal parsing only + import ast + result = ast.literal_eval(user_expression) + +### Path Traversal + # HIGH + def serve_file(filename): + return open(f"/var/www/files/{filename}").read() # ../../../etc/passwd works + + # Fix + import os + BASE_DIR = "/var/www/files" + + def serve_file(filename: str) -> str: + path = os.path.normpath(os.path.join(BASE_DIR, filename)) + if not path.startswith(BASE_DIR + os.sep): + raise PermissionError("Path traversal attempt blocked") + return open(path).read() + +### Hardcoded Secrets + # CRITICAL + API_KEY = "sk-abc123" # pragma: allowlist secret + DB_PASSWORD = "hunter2" # pragma: allowlist secret + + # Fix + import os + API_KEY = os.environ["API_KEY"] # crash-fast if missing + DB_PASSWORD = os.environ["DB_PASSWORD"] + +### Weak Crypto / Insecure Random + # HIGH — MD5 for passwords, predictable token generation + import hashlib, random + token = random.randint(100000, 999999) + pw_hash = hashlib.md5(password.encode()).hexdigest() + + # Fix + import secrets, bcrypt + token = secrets.token_urlsafe(32) + pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) + +### Unsafe Pickle + # CRITICAL — arbitrary code execution + import pickle + obj = pickle.loads(user_bytes) + + # Fix — use JSON for data exchange + import json + obj = json.loads(user_bytes) + +--- + +## 2. Common Bug Patterns + +### Mutable Default Argument + # HIGH + def add_item(item, container=[]): + container.append(item) + return container + + # Fix + def add_item(item: object, container: list | None = None) -> list: + if container is None: + container = [] + container.append(item) + return container + +### Late-Binding Closure + # HIGH — all lambdas return 4, not 0..4 + functions = [lambda: i for i in range(5)] + + # Fix — capture value at creation time + functions = [lambda i=i: i for i in range(5)] + +### Swallowing Exceptions + # HIGH + try: + risky() + except: # catches KeyboardInterrupt, SystemExit + pass + + # Fix + import logging + try: + risky() + except ValueError as e: + logging.warning("Validation failed: %s", e) + raise + +### Missing Context Manager + # MEDIUM + f = open("data.txt") + data = f.read() + f.close() # skipped if read() raises + + # Fix + with open("data.txt") as f: + data = f.read() + +### String Concatenation in Loop + # MEDIUM — O(n²) + result = "" + for chunk in chunks: + result += chunk + + # Fix + result = "".join(chunks) + +### Iterating While Mutating + # HIGH + for key in my_dict: + if condition(key): + del my_dict[key] # RuntimeError + + # Fix + my_dict = {k: v for k, v in my_dict.items() if not condition(k)} + +--- + +## 3. Pythonic Idioms (3.11) + +### None Comparisons + # Wrong + if x == None: ... + if x != None: ... + + # Right + if x is None: ... + if x is not None: ... + +### Empty Container Check + # Verbose + if len(items) == 0: ... + + # Pythonic + if not items: ... + +### Enumerate / Zip + # Old style + for i in range(len(items)): + print(i, items[i]) + + # Pythonic + for i, item in enumerate(items): + print(i, item) + + # Zip with length safety check (3.10+) + for a, b in zip(list_a, list_b, strict=True): + ... + +### Dict Merge (3.9+) + # Old + merged = {**defaults, **overrides} + + # Modern + merged = defaults | overrides + +### Structural Pattern Matching (3.10+) + # Verbose if/elif chain + if command == "quit": + quit_game() + elif command == "go" and direction in ("north", "south"): + go(direction) + elif command == "pick" and item: + pick(item) + + # match/case — clearer intent, exhaustiveness checkable + match command.split(): + case ["quit"]: + quit_game() + case ["go", ("north" | "south") as direction]: + go(direction) + case ["pick", item]: + pick(item) + case _: + print(f"Unknown command: {command}") + +### tomllib for TOML (3.11 stdlib) + # Old — required third-party package + import toml + config = toml.load("config.toml") + + # Modern — stdlib, no dependency needed + import tomllib + with open("config.toml", "rb") as f: + config = tomllib.load(f) + +### StrEnum (3.11) + # Old verbose pattern + from enum import Enum + class Color(str, Enum): + RED = "red" + GREEN = "green" + + # Modern + from enum import StrEnum + class Color(StrEnum): + RED = "red" + GREEN = "green" + +### Datetime UTC (deprecation in 3.12) + # Deprecated + from datetime import datetime + now = datetime.utcnow() + + # Correct — timezone-aware + from datetime import datetime, timezone + now = datetime.now(timezone.utc) + +### Pathlib + # String-based — fragile + import os + path = os.path.join(base, "subdir", "file.txt") + if os.path.exists(path): + with open(path) as f: + data = f.read() + + # Pathlib — composable and readable + from pathlib import Path + path = Path(base) / "subdir" / "file.txt" + if path.exists(): + data = path.read_text() + +--- + +## 4. Type Annotation Patterns (3.11) + +### Union Syntax + # Old (still valid, but verbose) + from typing import Optional, Union + def find(user_id: int) -> Optional[User]: ... + def process(value: Union[int, str]) -> None: ... + + # Modern (3.10+) + def find(user_id: int) -> User | None: ... + def process(value: int | str) -> None: ... + +### Self Return Type + # Old — fragile with subclassing + from typing import TypeVar + T = TypeVar("T", bound="Builder") + class Builder: + def set_name(self: T, name: str) -> T: ... + + # Modern (3.11) + from typing import Self + class Builder: + def set_name(self, name: str) -> Self: ... + +### Never for Unreachable Code + from typing import Never + def assert_never(x: Never) -> Never: + raise AssertionError(f"Unhandled value: {x}") + + # Use in exhaustive match + match status: + case "ok": handle_ok() + case "error": handle_error() + case _ as unreachable: + assert_never(unreachable) # mypy confirms exhaustiveness + +### LiteralString for SQL Safety (3.11) + from typing import LiteralString + + def execute(query: LiteralString) -> None: + cursor.execute(query) + + # mypy now rejects: execute(f"SELECT * FROM {user_input}") + # mypy accepts: execute("SELECT * FROM users WHERE id = %s") + +### TypedDict + # Untyped + def create_response(status: int, body: str) -> dict: ... + + # Typed + from typing import TypedDict + class Response(TypedDict): + status: int + body: str + headers: dict[str, str] + + def create_response(status: int, body: str) -> Response: ... + +--- + +## 5. Error Handling Patterns + +### Exception Chaining + # MEDIUM — original traceback lost + try: + db.connect() + except ConnectionError: + raise RuntimeError("Database unavailable") + + # Fix + try: + db.connect() + except ConnectionError as e: + raise RuntimeError("Database unavailable") from e + +### ExceptionGroup (3.11) — Concurrent Errors + # Multiple errors from concurrent tasks + errors = [] + for task in tasks: + try: + task.run() + except ValueError as e: + errors.append(e) + if errors: + raise ExceptionGroup("task failures", errors) + + # Handle specific types in the group + try: + run_all_tasks() + except* ValueError as eg: + for exc in eg.exceptions: + log.warning("Validation error: %s", exc) + except* IOError as eg: + for exc in eg.exceptions: + log.error("I/O error: %s", exc) + +### Custom Exception Hierarchy + # Too generic + raise Exception("User not found") + + # Better — specific, catchable by callers + class AppError(Exception): + """Base for all application errors.""" + + class NotFoundError(AppError): + """Resource does not exist.""" + + class ConfigError(AppError): + """Invalid or missing configuration.""" + + raise NotFoundError(f"User {user_id} not found") + +--- + +## 6. Testing Patterns + +### Correct Mock Target + # Wrong — mocking where requests is defined + from unittest.mock import patch + @patch("requests.get") + def test_fetch(mock_get): ... + + # Right — mock where it is used + @patch("mymodule.requests.get") + def test_fetch(mock_get): ... + +### Parametrize + # Repetitive + def test_valid_1(): assert validate("a@b.com") + def test_valid_2(): assert validate("x@y.org") + def test_invalid(): assert not validate("bad") + + # Concise + import pytest + @pytest.mark.parametrize("email,expected", [ + ("a@b.com", True), + ("x@y.org", True), + ("bad", False), + ]) + def test_validate(email, expected): + assert validate(email) == expected + +### Async Tests + import pytest, pytest_asyncio + + @pytest.mark.asyncio + async def test_fetch(): + result = await fetch_data() + assert result["status"] == "ok" + +--- + +## 7. Async Patterns + +### Blocking I/O in Async + # HIGH — blocks the event loop + async def fetch(): + response = requests.get(url) # sync, blocking + + # Fix + import httpx + async def fetch(): + async with httpx.AsyncClient() as client: + response = await client.get(url) + return response.json() + +### Fire-and-Forget Bug + # HIGH — exception from background_job() is silently discarded + async def handler(): + asyncio.create_task(background_job()) + + # Fix — store the task reference + _background_tasks: set[asyncio.Task] = set() + + async def handler(): + task = asyncio.create_task(background_job()) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) + +### TaskGroup (3.11) — Structured Concurrency + # Old — gather swallows some exceptions + results = await asyncio.gather(task1(), task2(), task3()) + + # Modern — all tasks cancelled if any fails, errors surface cleanly + async with asyncio.TaskGroup() as tg: + t1 = tg.create_task(task1()) + t2 = tg.create_task(task2()) + t3 = tg.create_task(task3()) + results = [t1.result(), t2.result(), t3.result()] + +### Run Blocking Sync Code in Async + # MEDIUM — CPU-bound work blocks the event loop + async def process(): + result = heavy_computation(data) + + # Fix + import asyncio + async def process(): + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, heavy_computation, data) + +--- + +## 8. Python 3.11 New Patterns + +### Fine-Grained Error Locations + # Python 3.11 tracebacks now show the exact expression that raised. + # This means multi-expression lines like: + result = obj.method().attr["key"] + # will point to the specific sub-expression (e.g. .attr) not just the line. + # Implication for review: split complex chains for better production debugging. + +### tomllib (No External Dependency) + # 3.11 ships tomllib in stdlib — remove tomli / toml from dependencies + import tomllib + with open("pyproject.toml", "rb") as f: + config = tomllib.load(f) + +### StrEnum + from enum import StrEnum, auto + class Direction(StrEnum): + NORTH = auto() # value becomes "north" (lowercased name) + SOUTH = auto() + + assert Direction.NORTH == "north" # True — no .value needed + +### TaskGroup (see Async section above) + +### exception.__notes__ (3.11) + # Attach context notes to an exception without subclassing + try: + process(record) + except ValueError as e: + e.add_note(f"Occurred while processing record id={record.id}") + raise diff --git a/template/.claude/skills/skill-maintainer/SKILL.md b/template/.claude/skills/skill-maintainer/SKILL.md new file mode 100644 index 0000000..e340260 --- /dev/null +++ b/template/.claude/skills/skill-maintainer/SKILL.md @@ -0,0 +1,419 @@ +--- +name: skill-maintainer +description: >- + Audit, standardize, and improve existing skills in template/.claude/skills/. Use this + skill when the user wants to: review or audit one or more skills, do a repo-wide health + check, standardize skills to a consistent format, fix outdated references, or improve + skill descriptions for better triggering. Triggers: 'audit this skill', 'review skill + quality', 'check skill health', 'standardize skills', 'skill repo maintenance', or any + request to systematically review or improve a SKILL.md file. Do NOT use for creating + brand-new skills from scratch — use the skill-creator skill instead. +--- + +# Skill Maintainer Skill + +Audit, standardize, and improve existing skills. This skill provides exact rules and +commands so any model — including haiku — can maintain skills mechanically. + +Skills live in `template/.claude/skills/`. Each skill is a directory: + +``` +template/.claude/skills/<skill-name>/ +├── SKILL.md # Required — frontmatter + instructions (< 400 lines) +├── references/ # Optional — deep-dive documents (one per topic) +│ └── <topic-name>.md # Descriptive kebab-case names, 200-270 lines each +└── scripts/ # Optional — executable tools + └── *.py or *.sh +``` + +--- + +## The six consistency dimensions + +Every skill MUST pass all six checks. These are the primary audit criteria. + +### Dimension 1 — Frontmatter block scalar + +The `description` field MUST use the `>-` block scalar (folds lines, strips trailing +newline). Not `>`, not a quoted string, not a plain string. + +```yaml +# CORRECT +description: >- + Comprehensive guide for writing pytest test cases... + +# WRONG — quoted string +description: "Comprehensive guide for writing pytest test cases..." + +# WRONG — plain > (keeps trailing newline) +description: > + Comprehensive guide for writing pytest test cases... +``` + +**Check:** +```bash +grep 'description:' template/.claude/skills/*/SKILL.md +``` +Every line must show `description: >-`. + +### Dimension 2 — Title format + +The first H1 heading MUST be `# <Name> Skill` where `<Name>` matches the skill's +identity in sentence case. + +```markdown +# Pytest Skill ← CORRECT +# Python Code Reviewer Skill ← CORRECT + +# pytest skill ← WRONG (not sentence case) +# Pytest ← WRONG (missing "Skill") +# Pytest Skill Guide ← WRONG (extra word) +``` + +**Check:** +```bash +grep '^# ' template/.claude/skills/*/SKILL.md | head -1 +``` +Every line must end with ` Skill`. + +### Dimension 3 — Reference table header + +Every SKILL.md MUST contain a section with the exact header: + +```markdown +## Quick reference: where to go deeper +``` + +This section MUST contain a pipe-delimited table with two columns: + +```markdown +| Topic | Reference file | +|------------------------|------------------------------------------------------| +| Descriptive topic name | [references/filename.md](references/filename.md) | +``` + +Column 1 is a human-readable topic description. Column 2 is a Markdown link using +relative path `references/<filename>.md`. Every file in `references/` MUST appear +in this table. No orphaned reference files. + +**Check:** +```bash +grep 'where to go deeper' template/.claude/skills/*/SKILL.md +``` +Every skill must have exactly one match. + +### Dimension 4 — Reference file naming + +All files in `references/` MUST use descriptive kebab-case names. No numbered prefixes. + +``` +fixtures.md ← CORRECT +anti-patterns.md ← CORRECT +document-structure.md ← CORRECT + +01-fixtures.md ← WRONG (numbered prefix) +Fixtures.md ← WRONG (uppercase) +fixtures_and_more.md ← WRONG (underscores) +``` + +**Check:** +```bash +find template/.claude/skills/*/references/ -name '[0-9]*' 2>/dev/null +``` +Must return zero results. + +### Dimension 5 — Reference H1 headings + +Every reference file MUST start with a sentence-case H1 heading. Capitalize the first +word and proper nouns only. + +```markdown +# Fixtures ← CORRECT +# Anti-patterns and fixes ← CORRECT +# CI, coverage, and plugins ← CORRECT + +# All About Fixtures ← WRONG (title case) +# FIXTURES ← WRONG (all caps) +# Python Patterns Reference (3.11+) ← WRONG (subtitle, version info) +``` + +**Check:** +```bash +for f in template/.claude/skills/*/references/*.md; do + echo "$(basename "$f"): $(head -1 "$f")" +done +``` +Inspect each heading for sentence case. + +### Dimension 6 — SKILL.md line count + +SKILL.md MUST be under 400 lines. Target range: 150-330 lines. If content exceeds +400 lines, move sections to `references/`. + +**Check:** +```bash +wc -l template/.claude/skills/*/SKILL.md +``` +Every file must show < 400. + +--- + +## Full verification script + +Run this after ANY skill edit. All six dimensions in one pass: + +```bash +SKILLS_DIR="template/.claude/skills" + +echo "=== 1. Frontmatter block scalar ===" +for f in "$SKILLS_DIR"/*/SKILL.md; do + skill=$(basename "$(dirname "$f")") + scalar=$(grep 'description:' "$f" | head -1 | sed 's/.*description: *//') + echo " $skill: $scalar" +done + +echo "" +echo "=== 2. Title format ===" +for f in "$SKILLS_DIR"/*/SKILL.md; do + skill=$(basename "$(dirname "$f")") + echo " $skill: $(grep '^# ' "$f" | head -1)" +done + +echo "" +echo "=== 3. Reference table header ===" +for f in "$SKILLS_DIR"/*/SKILL.md; do + skill=$(basename "$(dirname "$f")") + header=$(grep 'where to go deeper' "$f" || echo "(NOT FOUND)") + echo " $skill: $header" +done + +echo "" +echo "=== 4. Reference file naming (any numbered?) ===" +found=$(find "$SKILLS_DIR"/*/references/ -name '[0-9]*' 2>/dev/null) +[ -z "$found" ] && echo " None found ✓" || echo " $found" + +echo "" +echo "=== 5. Reference H1 headings ===" +for f in "$SKILLS_DIR"/*/references/*.md; do + skill=$(basename "$(dirname "$(dirname "$f")")") + echo " $skill/$(basename "$f"): $(head -1 "$f")" +done + +echo "" +echo "=== 6. SKILL.md line counts ===" +wc -l "$SKILLS_DIR"/*/SKILL.md +``` + +**Pass criteria:** ALL of these must be true: + +- Dimension 1: every skill shows `>-` +- Dimension 2: every title ends with ` Skill` +- Dimension 3: every skill has exactly one `where to go deeper` match +- Dimension 4: zero numbered files found +- Dimension 5: every H1 heading is sentence case +- Dimension 6: every SKILL.md is under 400 lines + +--- + +## Step-by-step: standardize a single skill + +Follow these steps exactly. Do not skip any step. + +### Step 1 — Inventory + +```bash +SKILL="<skill-name>" +SKILL_DIR="template/.claude/skills/$SKILL" + +# Check what exists +ls -la "$SKILL_DIR"/ +wc -l "$SKILL_DIR/SKILL.md" +ls "$SKILL_DIR/references/" 2>/dev/null || echo "No references/ directory" +ls "$SKILL_DIR/scripts/" 2>/dev/null || echo "No scripts/ directory" +``` + +### Step 2 — Read SKILL.md and note violations + +Read the full SKILL.md. Check each dimension against the rules above. Write down +every violation found. + +### Step 3 — Fix frontmatter (dimension 1) + +If the description uses a quoted string or `>` instead of `>-`, replace it. + +**Before (quoted string):** +```yaml +description: "Audit and improve skills..." +``` + +**After:** +```yaml +description: >- + Audit and improve skills... +``` + +**Before (plain >):** +```yaml +description: > + Audit and improve skills... +``` + +**After (add the dash):** +```yaml +description: >- + Audit and improve skills... +``` + +### Step 4 — Fix title (dimension 2) + +If the H1 heading does not end with ` Skill`, fix it. + +**Before:** +```markdown +# Python Code Reviewer (Python 3.11+) +``` + +**After:** +```markdown +# Python Code Reviewer Skill +``` + +Remove version numbers, subtitles, and parenthetical notes from the title. +Move that information into the intro paragraph below the title. + +### Step 5 — Fix reference table (dimension 3) + +If the reference section uses a different header or format, replace it. + +**Before (wrong header, wrong format):** +```markdown +## Reference Files (load on-demand) + +| File | When to load | +|------|-------------| +| `references/checklist.md` | Deep mode reviews | +``` + +**After (correct header, correct format):** +```markdown +## Quick reference: where to go deeper + +| Topic | Reference file | +|------------------------------------|--------------------------------------------------------| +| Full 13-category review checklist | [references/checklist.md](references/checklist.md) | +``` + +If other sections use the word "Quick reference" (e.g., a dispatch table), rename +those to avoid collision. Example: rename `## Quick Reference` to `## Scope dispatch`. + +### Step 6 — Fix reference file names (dimension 4) + +If any reference files have numbered prefixes, rename them. + +```bash +# Rename numbered files +cd "$SKILL_DIR/references/" +mv 01-document-structure.md document-structure.md +mv 02-formatting-syntax.md formatting-syntax.md +``` + +After renaming, update ALL links in SKILL.md that reference the old filenames. + +### Step 7 — Fix reference H1 headings (dimension 5) + +Read line 1 of each reference file. If not sentence case, fix it. + +**Before:** +```markdown +# Python Review Checklist — Full Detail (Python 3.11+) +``` + +**After:** +```markdown +# Review checklist +``` + +Rules for sentence case: capitalize first word and proper nouns (Python, Django, +FastAPI, GitHub, etc.) only. Everything else is lowercase. + +### Step 8 — Fix line count (dimension 6) + +If SKILL.md exceeds 400 lines, identify sections that are: +- Detailed reference material (not needed on every invocation) +- Long code example blocks +- Extended checklists or tables + +Move those sections to new files in `references/`. Add a row to the quick-reference +table for each new file. + +### Step 9 — Run verification + +Run the full verification script from above. ALL six dimensions must pass. + +--- + +## Step-by-step: repo-wide audit + +```bash +# List all skills +ls template/.claude/skills/ + +# Run full verification (all six dimensions) +# Copy and run the verification script above + +# Check for broken reference links +for f in template/.claude/skills/*/SKILL.md; do + skill_dir=$(dirname "$f") + grep -oP 'references/[a-z0-9-]+\.md' "$f" | while read ref; do + [ -f "$skill_dir/$ref" ] || echo "BROKEN: $skill_dir/$ref" + done +done + +# Check for orphaned reference files (not linked from SKILL.md) +for dir in template/.claude/skills/*/references; do + skill_md="$(dirname "$dir")/SKILL.md" + for ref in "$dir"/*.md; do + basename_ref=$(basename "$ref") + grep -q "$basename_ref" "$skill_md" || echo "ORPHANED: $ref" + done +done +``` + +Fix issues in priority order: broken links first, then dimension violations, +then orphaned files. + +--- + +## SKILL.md section order + +When restructuring a SKILL.md, use this standard order: + +1. YAML frontmatter (`---` block with `name` and `description: >-`) +2. `# <Name> Skill` title +3. 1-3 line intro paragraph +4. Core content sections (main workflows, principles, checklists) +5. `## Quick reference: where to go deeper` (reference table) +6. Optional: `## Bundled scripts` (if `scripts/` exists) + +--- + +## Writing style rules + +Apply these when editing any skill content: + +- **Sentence case headings** — capitalize first word and proper nouns only +- **Imperative voice** — "Run this command" not "This command can be run" +- **Short paragraphs** — 3-5 lines max; use tables for parallel items +- **100-character line width** — wrap prose, not code blocks or tables +- **No emoji in headings** — emoji are allowed in table cells for quick scanning +- **Relative links** — use `[references/file.md](references/file.md)`, not absolute paths + +--- + +## Quick reference: where to go deeper + +| Topic | Reference file | +|--------------------------------------|----------------------------------------------------------------------------| +| Full 9-area deep audit checklist | [references/audit-checklist.md](references/audit-checklist.md) | +| Annotated before/after fix examples | [references/audit-examples.md](references/audit-examples.md) | +| Running log of all audit sessions | [references/maintenance-log.md](references/maintenance-log.md) | diff --git a/template/.claude/skills/skill-maintainer/references/audit-checklist.md b/template/.claude/skills/skill-maintainer/references/audit-checklist.md new file mode 100644 index 0000000..e8535db --- /dev/null +++ b/template/.claude/skills/skill-maintainer/references/audit-checklist.md @@ -0,0 +1,234 @@ +# Audit checklist + +Full 9-area deep audit. Run all areas for each skill, top to bottom. Use for +thorough reviews, repo-wide sweeps, or when unsure about a specific quality area. + +The six consistency dimensions (in SKILL.md) are the minimum bar. This checklist +goes deeper into content quality, technical accuracy, and cross-skill health. + +--- + +## Severity rubric + +Use consistently when labelling issues: + +| Severity | Definition | Examples | +|----------|------------|---------| +| **HIGH** | Skill won't trigger, produces broken output, or has broken links | Missing/vague description, dangling reference, corrupt script | +| **MED** | Degrades quality, causes confusion, or routes to wrong skill | Stale examples, missing NOT triggers, passive writing style | +| **LOW** | Cosmetic, organizational, or future-proofing | Line count creep, minor heading case error, hypothetical examples | + +--- + +## Area 1 — Frontmatter quality + +Read the YAML frontmatter at the top of SKILL.md. + +**Required fields:** `name` (lowercase, hyphenated) and `description` (using `>-`). + +**Description quality checklist:** + +| Criterion | Good | Bad | +|-----------|------|-----| +| Specific trigger phrases | "triggers on 'Word doc', '.docx'" | "use for document tasks" | +| Explicit NOT/DO NOT list | "Do NOT use for PDFs" | no exclusions at all | +| Covers implicit requests | "If user asks for a 'report' as a Word file" | only explicit mentions | +| Imperative phrasing | "Use this skill whenever..." | "This skill can be used..." | +| Word count | 80-150 words | < 30 or > 200 | + +**Collision check** — compare descriptions across skills for overlapping triggers: + +```bash +grep -A5 "^description:" template/.claude/skills/*/SKILL.md +``` + +If two skills claim the same trigger, both need NOT clauses naming the other. + +--- + +## Area 2 — Structure and navigation + +- **Clear intro** — 1-3 lines after the title explaining what the skill does +- **Logical headers** — H2 for major sections, H3 for subsections, no skipped levels +- **Under 400 lines** — if longer, move content to `references/` +- **Reference table present** — `## Quick reference: where to go deeper` with table +- **No dangling references** — every linked file actually exists + +```bash +SKILL_DIR="template/.claude/skills/<skill-name>" + +# Line count +wc -l "$SKILL_DIR/SKILL.md" + +# Check all reference links resolve +grep -oP 'references/[a-z0-9-]+\.md' "$SKILL_DIR/SKILL.md" | while read ref; do + [ -f "$SKILL_DIR/$ref" ] || echo "BROKEN: $ref" +done +``` + +**Writing style** — instructions use imperative form ("Run this command", "Set the +page size"), not passive ("This command can be run", "The page size should be set"). +Flag skills that mix tenses or are mostly passive. + +--- + +## Area 3 — Technical accuracy + +The most perishable part of any skill. + +**Package versions and install commands:** +```bash +# pip packages +pip index versions <package-name> --break-system-packages 2>/dev/null | head -3 + +# npm packages +npm view <package-name> version +``` + +Flag when a skill pins an old major version and the new version has breaking changes. + +**Script paths** — verify every script reference exists: +```bash +ls template/.claude/skills/<skill-name>/scripts/ 2>/dev/null +``` + +**API/syntax accuracy** — spot-check 2-3 code examples against the current library +API. Flag method renames, removed parameters, or changed import paths. + +--- + +## Area 4 — Script quality + +For skills with a `scripts/` directory: + +**Runnability:** +```bash +python template/.claude/skills/<skill>/scripts/<script>.py --help 2>&1 | head -5 +``` + +**Error handling** — scripts should fail with clear messages, not Python tracebacks. + +**Undocumented dependencies:** +```bash +grep -E "^import|^from" template/.claude/skills/<skill>/scripts/*.py \ + | grep -v "os\|sys\|json\|re\|pathlib\|argparse\|dataclasses" +``` + +Every non-stdlib import must be mentioned in the skill's documentation or have a +try/import guard with an install instruction. + +**pip convention** — scripts that install packages must use `--break-system-packages`. + +--- + +## Area 5 — Cross-skill consistency + +Skills sometimes reference each other. Stale cross-references cause wrong routing. + +**Verify all cross-references:** +```bash +grep -rn "skills/" template/.claude/skills/ --include="*.md" | grep -v ".skill:" +``` + +When a new skill splits an existing skill's domain, update ALL related skills' +descriptions and reference tables on the same day. + +--- + +## Area 6 — Progressive disclosure compliance + +| Layer | Location | Target size | Check | +|-------|----------|-------------|-------| +| Metadata | YAML frontmatter | 80-150 words | Not a wall of text | +| Core | SKILL.md body | < 400 lines | `wc -l` | +| Deep reference | `references/` | 200-270 lines each | Per-file check | +| Scripts | `scripts/` | Unlimited | Documented in SKILL.md | + +**Red flags:** +- SKILL.md > 400 lines with no `references/` directory +- Critical info buried past line 300 with no reference table near the top +- Same gotcha repeated 3+ times instead of consolidated +- Reference files listed in SKILL.md but never linked with Markdown links + +--- + +## Area 7 — Critical rules and pitfall coverage + +Every practical skill should have a section surfacing 5-10 non-obvious gotchas. + +Quality check: are rules specific and actionable? + +``` +VAGUE: "Always validate output" +ACTIONABLE: "After saving, load the file back with openpyxl and check for #REF! errors" +``` + +--- + +## Area 8 — Output and validation + +For skills that produce files or modify code: + +- Is there a validation step after the action? +- Do code examples include error handling? +- Is there guidance on what to do when something fails? + +**Template for adding a missing validation step:** +```markdown +### Validation + +After creating the file, validate before delivering: +[specific command or code block] +If validation fails: [specific recovery steps] +``` + +--- + +## Area 9 — Security review + +Quick pass — flag anything surprising for human review: + +- Bash commands that send data to external servers +- Code that accesses credentials or sensitive files +- Description that does not accurately reflect what the skill does +- Scripts that download or execute code from the network + +--- + +## Producing an audit report + +After completing all 9 areas: + +```markdown +## Skill audit: <skill-name> +**Reviewed:** <date> + +### Overall health: [Good / Needs work / Critical issues] + +### Issues found +| # | Area | Severity | Description | +|---|------|----------|-------------| +| 1 | Frontmatter | HIGH | Description uses quoted string, not >- | +| 2 | Structure | MED | SKILL.md is 480 lines, needs reference extraction | +| 3 | Headings | LOW | Reference file uses title case instead of sentence case | + +### Recommended fixes (priority order) +1. [HIGH] Change description to >- block scalar +2. [MED] Move lines 300-480 to references/deep-dive.md +3. [LOW] Change "# All About Fixtures" to "# Fixtures" + +### Strengths to preserve +- Excellent reference table with clear topic descriptions +- Scripts are well-documented with --help flags +``` + +For repo-wide audits, lead with a summary: + +```markdown +## Repo health summary — <date> +| Skill | Health | High | Med | Low | Top issue | +|-------|--------|------|-----|-----|-----------| +| pytest | Good | 0 | 0 | 1 | Minor heading case | +| markdown | Good | 0 | 0 | 0 | Clean | +| python-code-quality | Good | 0 | 1 | 0 | Section header inconsistency | +``` diff --git a/template/.claude/skills/skill-maintainer/references/audit-examples.md b/template/.claude/skills/skill-maintainer/references/audit-examples.md new file mode 100644 index 0000000..a4e6e9a --- /dev/null +++ b/template/.claude/skills/skill-maintainer/references/audit-examples.md @@ -0,0 +1,233 @@ +# Audit examples: before & after + +Real patterns from this repo. Use these as templates when applying fixes. + +--- + +## Example 1: Description Missing NOT Clause — `pptx` + +**Skill:** `pptx` + +**Before (abridged):** +```yaml +description: "Use this skill any time a .pptx file is involved in any way — as input, output, +or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, +or extracting text from any .pptx file..." +``` + +**Problem:** The description triggers on any `.pptx` task, including read-only analysis tasks where `file-reading` would be more appropriate as the entry point. No exclusion clause. + +**After:** +```yaml +description: "Use this skill any time a .pptx file is involved in any way — as input, output, +or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, +or extracting text from any .pptx file (even if the extracted content will be used elsewhere, +like in an email or summary); editing, modifying, or updating existing presentations; combining +or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger +whenever the user mentions 'deck,' 'slides,' 'presentation,' or references a .pptx filename. +Do NOT use if the file is .ppt (legacy) without converting first — convert to .pptx before this +skill can work. Do NOT use for Google Slides — this skill is .pptx only." +``` + +**What changed:** Added two explicit NOT clauses. First prevents silent failures on legacy `.ppt` files (python-pptx rejects them). Second prevents wrong triggers on Google Slides tasks. + +**Lesson:** NOT clauses aren't just for collision avoidance — they also prevent silent technical failures where the skill can't handle the input format. + +--- + +## Example 2: Description Collision Between Sibling Skills — `pdf` and `pdf-reading` + +**Skills:** `pdf` and `pdf-reading` + +**Before — `pdf` description (abridged):** +```yaml +description: "Use this skill whenever the user wants to do anything with PDF files. +This includes reading or extracting text/tables from PDFs..." +``` + +**Before — `pdf-reading` description:** +```yaml +description: "Use this skill when you need to read, inspect, or extract content from PDF files..." +``` + +**Problem:** Both descriptions claimed "reading PDFs." Claude was triggering `pdf` for read-only analysis tasks, routing users to the creation-focused skill with heavier dependencies. + +**After — `pdf` description fix:** +Change "reading or extracting" to "processing, transforming, creating, or manipulating" and add: +> "Do NOT use for read-only PDF analysis — use the pdf-reading skill instead." + +**After — `pdf-reading` description fix:** +Already has the NOT clause correctly: +> "Do NOT use this skill for PDF creation, form filling, merging, splitting, watermarking, or encryption — use the pdf skill instead." + +**Lesson:** Boundary clarity must be maintained in BOTH directions. Each skill's NOT list should name the other skill explicitly, not just describe what it doesn't do. + +--- + +## Example 3: Monolithic SKILL.md → Layered Structure — `docx` + +**Skill:** `docx` — SKILL.md reached 590 lines + +**The problem section:** The XML Reference (lines 455–590) contained detailed XML patterns for tracked changes, comments, and images. This content is only needed when hand-editing XML directly — maybe 10% of use cases. + +**Fix:** +1. Created `references/xml-reference.md` and moved lines 455–590 there +2. Replaced the section in SKILL.md with a pointer: + +```markdown +## XML Reference + +For full XML patterns (tracked changes, comments, images, schema compliance): +see `references/xml-reference.md`. + +Read it when: you need to hand-edit `word/document.xml` directly rather than +using the docx-js API or unpack/pack scripts. +``` + +**Result:** SKILL.md went from 590 → 395 lines. The XML content is still available but only loaded when actually needed. + +**What to watch for:** After moving content, verify SKILL.md still has enough context that a user can accomplish the most common tasks (creating a document, basic editing) without reading the reference file. The reference is for depth, not basics. + +--- + +## Example 4: Broken Dispatch Table — `file-reading` + +**Skill:** `file-reading` dispatch table + +**Before:** +```markdown +| `.pdf` | Content inventory | `/mnt/skills/public/pdf/SKILL.md` | +``` + +**Problem:** `pdf-reading` was added as a dedicated read-only skill after `file-reading` was written. The dispatch table still pointed all PDF tasks to the creation-focused `pdf` skill. + +**Fix:** +```markdown +| `.pdf` (read/extract/analyze) | Content inventory → pdf-reading skill | `/mnt/skills/public/pdf-reading/SKILL.md` | +| `.pdf` (create/merge/split/encrypt) | — | `/mnt/skills/public/pdf/SKILL.md` | +``` + +**Process:** After fixing `file-reading`, also updated `pdf-reading` to say "For file-reading routing, you'll arrive here from the file-reading dispatch table" so the flow is documented end-to-end. + +**Lesson:** When a new skill is added that splits an existing skill's domain, update ALL dispatch tables in adjacent skills on the same day. Drift here causes user confusion that's very hard to diagnose. + +--- + +## Example 5: Stale Dependency — `docx` npm package + +**Skill:** `docx` + +**Before:** +```bash +npm install -g docx@6.2.0 +``` +And code using: +```javascript +const { Document, Packer } = require('docx'); +``` + +**Problem:** `docx` v7+ changed `Packer.toBuffer(doc)` to `Packer.toBuffer(doc)` returning a Buffer directly (previously returned a Promise of Buffer with different handling). Code written for v6 silently produced wrong output on v7+. + +**Audit process:** +```bash +npm view docx version # revealed: 8.5.0 +``` +Changelog showed v7 broke the Promise-based Packer pattern. + +**Fix:** +1. Updated install: `npm install -g docx` (no pinned version) +2. Tested updated pattern in bash_tool to confirm it works +3. Updated all Packer usages in examples +4. Added version comment: `// docx@7+ API — Packer returns Buffer directly` + +**Lesson:** Pin to a major version only when documenting a specific known breaking change. Omit version pins otherwise — stale installs are worse than no pin. + +--- + +## Example 6: Missing Validation Step — `xlsx` + +**Skill:** `xlsx` + +**Before:** Skill had thorough instructions for creating spreadsheets with openpyxl, but ended with: +```python +wb.save("output.xlsx") +``` +No validation. No mention of what to do if the file was corrupt. + +**Problem:** openpyxl can create structurally valid `.xlsx` files that Excel/Google Sheets refuses to open due to formula errors, broken cell references, or missing named ranges. Without validation, users received silent corrupt outputs. + +**Fix — added after the save call:** +```markdown +### Validation + +After saving, verify the file has no formula errors before presenting to the user: +```python +from openpyxl import load_workbook + +wb = load_workbook("output.xlsx", data_only=False) +errors = [] +for sheet in wb.sheetnames: + ws = wb[sheet] + for row in ws.iter_rows(): + for cell in row: + if cell.value and str(cell.value).startswith(('#REF!', '#DIV/0!', '#VALUE!', '#N/A', '#NAME?')): + errors.append(f"{sheet}!{cell.coordinate}: {cell.value}") + +if errors: + print("Formula errors found:", errors) + # Fix the formula or data before delivering +else: + print("File validated — no formula errors") +``` +**Zero formula errors is a hard requirement for all Excel deliverables.** +``` + +--- + +## Example 7: Passive Writing Style — Generic Example + +**Pattern found across several skills:** + +**Before (passive):** +```markdown +The document can be unpacked using the following command. +Content should be extracted before editing. +Styles can be overridden by using exact IDs. +``` + +**After (imperative):** +```markdown +Unpack the document before editing: +Extract content first, then make edits. +Override styles using exact IDs — "Heading1", "Heading2", etc. +``` + +**Why this matters:** The skill-creator standard says imperative form reduces cognitive load. A passive instruction ("can be done") suggests optionality. An imperative instruction ("do this") signals required steps. Skills are instructions, not descriptions — they should read like instructions. + +**Quick check:** If you find yourself writing "can be", "should be", or "is able to", rewrite as a direct command. + +--- + +## Example 8: Script Quality — Undocumented Dependency + +**Skill:** `pdf` — `scripts/ocr.py` + +**Before:** Script used `pytesseract` but this wasn't listed in the skill's Dependencies section and wasn't in the standard environment. + +**Discovered via:** +```bash +grep "^import\|^from" /mnt/skills/public/pdf/scripts/ocr.py | grep -v "os\|sys\|json" +# Output: from pytesseract import image_to_string +``` + +**Fix:** +1. Added to Dependencies section: `pytesseract: OCR fallback (install: pip install pytesseract --break-system-packages)` +2. Added install-check at the top of the script: +```python +try: + import pytesseract +except ImportError: + raise SystemExit("pytesseract not installed. Run: pip install pytesseract --break-system-packages") +``` + +**Lesson:** Always grep script imports for non-stdlib packages and verify every one appears in the skill's Dependencies section. An undocumented dependency causes a `ModuleNotFoundError` with no guidance on how to fix it. diff --git a/template/.claude/skills/skill-maintainer/references/maintenance-log.md b/template/.claude/skills/skill-maintainer/references/maintenance-log.md new file mode 100644 index 0000000..03dfe84 --- /dev/null +++ b/template/.claude/skills/skill-maintainer/references/maintenance-log.md @@ -0,0 +1,19 @@ +# Skill repo maintenance log + +Track every audit session here. Append a row after each review. + +## Format + +| Date | Scope | Skills Reviewed | Issues Found | Issues Fixed | Notes | +|------|-------|----------------|--------------|--------------|-------| +| YYYY-MM-DD | single: skill-name OR repo-wide | skill-name OR all N | XH, YM, ZL | XH, YM, ZL | What was deferred and why | + +## Severity key: H = HIGH, M = MED, L = LOW + +--- + +## Log + +| Date | Scope | Skills Reviewed | Issues Found | Issues Fixed | Notes | +|------|-------|----------------|--------------|--------------|-------| +| 2026-04-09 | Repo-wide | skill-maintainer | 4H, 5M, 4L | 4H, 5M, 4L | Initial creation + full audit of own skill |