@ericrisco/rsc 0.1.46 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.46",
2
+ "version": "0.1.48",
3
3
  "counts": {
4
4
  "skills": 258
5
5
  },
@@ -1136,7 +1136,7 @@
1136
1136
  },
1137
1137
  {
1138
1138
  "id": "db-migrations",
1139
- "description": "Use when planning a schema change that must ship without downtime — adding a NOT NULL column, renaming/splitting/merging a column or table, changing a type, or backfilling millions of rows on a live table — and you need the expand-contract sequence plus the lock/timeout/batching discipline that keeps each step from freezing prod. Triggers: 'zero-downtime migration', 'add NOT NULL column to a huge table', 'rename a column safely', 'our deploy keeps deadlocking when we add an index', 'backfill 80M rows without replication lag', 'expand contract', 'gh-ost vs pt-osc', 'migración sin downtime', 'migració sense tallar el servei'. NOT Postgres lock internals/EXPLAIN/RLS (that is postgresdb), NOT drizzle-kit runner mechanics (that is drizzle-orm), NOT restore drills/PITR (that is backups).",
1139
+ "description": "Use when a schema change must ship without downtime — NOT NULL, rename, type change, or backfilling millions of live rows — for the expand-contract sequence and the lock/batching discipline that keeps each step from freezing prod. NOT lock internals or EXPLAIN (that is `postgresdb`), NOT drizzle-kit mechanics (that is `drizzle-orm`), NOT PITR (that is `backups`).",
1140
1140
  "tags": [
1141
1141
  "migrations",
1142
1142
  "zero-downtime",
@@ -1170,7 +1170,7 @@
1170
1170
  },
1171
1171
  {
1172
1172
  "id": "decision-records",
1173
- "description": "Use when a consequential, hard-to-reverse choice was just made or is about to be — database, framework, auth model, vendor, API style, deployment target — and future-you will ask why, with the answer buried in chat; capture it as a numbered, immutable ADR with the context that forced the choice, the real options weighed with their trade-offs, the decision, and consequences both ways. Also when a prior decision is being reversed and must be superseded without erasing history, or when onboarding keeps re-litigating a settled choice. Triggers: 'write up why we picked Postgres over DynamoDB so we stop arguing', 'record the switch to tRPC but keep the old REST decision in history', 'documenta por qué elegimos Hetzner y qué alternativas descartamos con pros y contras', 'registra la decisió i les alternatives que vam descartar'. NOT the meeting recap with action items and owners (that is meeting-notes), NOT the project-wide standing principles and stack canon (that is constitution).",
1173
+ "description": "Use when a hard-to-reverse choice (database, framework, vendor, auth model) must be frozen as an immutable numbered ADR — the context that forced it, the options weighed, the decision, consequences both ways — or superseded without erasing history. NOT a meeting recap (that is meeting-notes), NOT standing project principles (that is constitution).",
1174
1174
  "tags": [
1175
1175
  "decision-records",
1176
1176
  "adr",
@@ -1193,7 +1193,7 @@
1193
1193
  },
1194
1194
  {
1195
1195
  "id": "deep-learning",
1196
- "description": "Use when training or debugging a neural network in PyTorch — writing the training loop, chasing loss that won't drop or goes NaN, adding mixed precision (AMP), scaling across GPUs, or making a run reproducible. Covers tensors/autograd/nn.Module, torch.compile, the forward→loss→backward→step→zero_grad loop and its silent bugs, AdamW + cosine/warmup schedulers, DDP vs FSDP vs DeepSpeed ZeRO, checkpointing and seeds. PyTorch-first (JAX/TF exist). Triggers: 'my training loss isn't decreasing', 'loss becomes NaN after a few steps', 'how do I use torch.autocast and GradScaler', 'train this model across 4 GPUs', 'val accuracy is worse than train and I forgot model.eval()', 'gradients seem to add up across batches', 'build a CNN from scratch and train it', 'entrenar una xarxa neuronal des de zero amb PyTorch'. NOT adapting a pretrained LLM with LoRA/QLoRA (that is finetuning); NOT classic/tabular models like XGBoost or sklearn (that is machine-learning); NOT tokenization or NLP task metrics (that is nlp).",
1196
+ "description": "Use when training or debugging a neural net in PyTorch — the forward/loss/backward/step loop and its silent bugs, mixed precision (AMP), AdamW/LR schedules, DDP/FSDP/ZeRO, checkpoints and seeds. NOT LoRA/QLoRA on a pretrained LLM (that is `finetuning`), NOT tabular sklearn/XGBoost (that is `machine-learning`), NOT tokenization or NLP metrics (that is `nlp`).",
1197
1197
  "tags": [
1198
1198
  "pytorch",
1199
1199
  "deep-learning",
@@ -1250,7 +1250,7 @@
1250
1250
  },
1251
1251
  {
1252
1252
  "id": "digitalocean",
1253
- "description": "Use when deploying or operating a workload on DigitalOcean and you must decide where it runs and how to ship it — Droplet (raw VPS) vs App Platform (managed PaaS) vs Functions, wiring a Managed Database (Postgres/MySQL/Valkey), Spaces object storage + CDN, doctl auth, App Platform app spec YAML, cloud firewalls, VPC private networking, reserved IPs, snapshots/backups, app logs, scaling. Triggers: 'deploy to DigitalOcean', 'DigitalOcean App Platform app spec', 'DO managed postgres with connection pooling', 'doctl apps create', 'reserved IP failover', 'I need an S3-compatible bucket with a CDN' (that is Spaces — non-obvious), 'subir mi web a DigitalOcean con doctl', 'Droplet o App Platform, qué sale más barato'. NOT host-agnostic CI/CD or rollback strategy (that is deployment), NOT a Hetzner bare VPS (that is hetzner).",
1253
+ "description": "Use when deploying or operating a workload on DigitalOcean — Droplet vs App Platform vs Functions, doctl, app spec YAML, Managed Postgres/MySQL/Valkey on the VPC, S3-compatible Spaces + CDN. NOT host-agnostic CI/CD or rollback strategy (that is `deployment`), NOT a bare Hetzner VPS (that is `hetzner`), NOT another managed PaaS (that is `railway`).",
1254
1254
  "tags": [
1255
1255
  "digitalocean",
1256
1256
  "doctl",
@@ -1274,7 +1274,7 @@
1274
1274
  },
1275
1275
  {
1276
1276
  "id": "django",
1277
- "description": "Use when building, reviewing, securing, testing or shipping a Django web app — models, migrations, QuerySets, managers, FBV/CBV views, forms, the admin, settings split, and Django REST Framework (serializers, ModelViewSet, routers, permissions, throttling). Triggers: 'add a filter and fix the N+1 in this ListView', 'build a DRF ModelViewSet with owner-only permission', 'this template loops over books and queries book.author each iteration' (N+1 without the word Django), 'why does manage.py check --deploy warn about SECURE_HSTS_SECONDS', 'revisa este modelo y añade un UniqueConstraint en (tenant, slug)', 'el queryset hace N+1', any file with manage.py, settings.py, models.py, serializers.py or a 0001_initial migration. NOT async-first FastAPI services (that is fastapi).",
1277
+ "description": "Use when building, reviewing, securing, testing or shipping a Django app — models, migrations, QuerySets/managers, FBV/CBV views, forms, the admin, settings split, and Django REST Framework (serializers, ModelViewSet, permissions). NOT async FastAPI/Pydantic services (that is `fastapi`), NOT Postgres schema/index work (that is `postgresdb`).",
1278
1278
  "tags": [
1279
1279
  "python",
1280
1280
  "django",
@@ -1294,7 +1294,7 @@
1294
1294
  },
1295
1295
  {
1296
1296
  "id": "docker",
1297
- "description": "Use when authoring or auditing a Dockerfile/Containerfile, shrinking a bloated image, hardening a container that runs as root, choosing a base image, or wiring a Compose dev loop with hot reload. Triggers: 'dockerize this app', 'my node image is 1.2GB make it smaller', 'the build cache keeps busting', 'harden this Dockerfile, trivy flags it', 'docker compose watch for local dev', 'distroless vs alpine vs slim', 'la imagen pesa demasiado', 'el build de docker tarda siglos'. NOT the CI/CD pipeline or deploy-to-host (that is deployment), NOT k8s autoscaling/HPA (that is scaling), NOT app-level SQL injection or secrets-in-code (that is secure-coding).",
1297
+ "description": "Use when authoring or auditing a Dockerfile, shrinking a bloated image, hardening a container that runs as root, picking a base image, or wiring a Compose dev loop with hot reload. NOT CI builds or deploy-to-host (that is `deployment`), NOT k8s autoscaling (that is `scaling`), NOT app-level injection or secrets-in-code (that is `secure-coding`).",
1298
1298
  "tags": [
1299
1299
  "docker",
1300
1300
  "containers",
@@ -1312,7 +1312,7 @@
1312
1312
  },
1313
1313
  {
1314
1314
  "id": "document-processing",
1315
- "description": "Use when you need to get content out of a document or build a document out of data — extracting text/tables from PDFs, filling interactive PDF forms (AcroForm), generating PDF/DOCX from templates, or OCR'ing scanned/image-only files into Markdown/JSON. Triggers: 'extract the line items from this invoice PDF', 'fill this PDF form and flatten it', 'generate a contract DOCX from this template', 'this scan has no text layer', 'the PDF copies out as garbage symbols', 'merge/split/rotate these PDFs', 'OCR 300 scanned pages', 'rellena este formulario PDF y fusiónalos', 'extreu les taules d'aquesta factura escanejada'. NOT pulling schema-typed fields out of text (that is structured-extraction), NOT sending it for signature (that is e-signature), NOT reading spreadsheet cells/formulas (that is spreadsheet-ops).",
1315
+ "description": "Use when the deliverable is a document's bytes or its literal content — text/tables out of PDFs, AcroForm fill and flatten, page merge/split, PDF/DOCX from templates, OCR of image-only scans. NOT schema-typed fields pulled from text (that is structured-extraction), NOT signature routing (e-signature) or spreadsheet cells/formulas (spreadsheet-ops).",
1316
1316
  "tags": [
1317
1317
  "pdf",
1318
1318
  "ocr",
@@ -1448,7 +1448,7 @@
1448
1448
  },
1449
1449
  {
1450
1450
  "id": "e2e-testing",
1451
- "description": "Use when writing or stabilizing browser end-to-end tests with Playwright — driving real navigation, forms, logins, and multi-step user journeys through an actual browser, picking durable locators, or killing tests that pass locally but flake in CI. Covers getByRole/getByTestId locator choice, web-first assertions, storageState auth via setup projects, retries/trace config, sharding, and the 2026 flakiness playbook. Triggers: 'write a Playwright test', 'my e2e tests flake in CI but pass locally', 'set up storageState so I don't log in every test', 'getByRole vs CSS selector', 'shard Playwright across machines', 'trace on retry', 'strict mode resolved to 2 elements', 'els tests d'extrem a extrem fallen aleatòriament al pipeline', 'los tests e2e fallan a veces'. NOT in-process component/unit web tests (that is testing-web), NOT pytest suites (that is testing-py), NOT a11y audits as the goal (that is accessibility), NOT building the CI pipeline itself (that is github-actions).",
1451
+ "description": "Use when writing or stabilizing Playwright tests that drive a real browser through multi-step journeys — durable locators, web-first assertions, storageState auth, trace/retries, and flakes that only bite in CI. NOT in-process component tests (that is testing-web), NOT WCAG auditing (that is accessibility), NOT the pre-merge gate (that is verify).",
1452
1452
  "tags": [
1453
1453
  "playwright",
1454
1454
  "e2e",
@@ -1508,7 +1508,7 @@
1508
1508
  },
1509
1509
  {
1510
1510
  "id": "email-connector",
1511
- "description": "Use when wiring an app to send transactional or bulk email through an HTTP provider (Resend, Twilio SendGrid, Postmark) from server code: welcome/reset/receipt sends, a provider-agnostic sendEmail() seam, React Email or dynamic templates, idempotent retries, 100-cap batch with partial-failure handling, transactional-vs-broadcast stream split, and the bounce/complaint webhook feeding a suppression list. Triggers: 'send a password-reset email with Resend', 'emails double-send when the queue retries', 'switch us off SendGrid onto Postmark without rewriting every call site', 'send a digest to 4000 users and handle the ones that fail', 'el webhook de bounces no actualiza la lista de supresión', 'montar el envío de correos con Resend'. NOT writing the subject line or the open/click growth system (that is newsletter).",
1511
+ "description": "Use when wiring server code to send transactional or bulk email via Resend, SendGrid, or Postmark: a provider-agnostic sendEmail() seam, idempotent retries, 100-cap batches with partial failures, transactional-vs-broadcast streams, bounce webhooks feeding a suppression list. NOT SPF/DKIM/DMARC inbox reputation (that is `email-deliverability`).",
1512
1512
  "tags": [
1513
1513
  "email",
1514
1514
  "transactional-email",
@@ -1575,7 +1575,7 @@
1575
1575
  },
1576
1576
  {
1577
1577
  "id": "error-handling",
1578
- "description": "Use when designing how code recognizes, recovers from, and reports failure — typed error taxonomies, retry/backoff/timeout policy, circuit breakers, React/Next error boundaries, and the user message + operator log line. Triggers: 'add retries with backoff', 'set up an error boundary', 'standardize our error types', 'stop swallowing exceptions', 'this catch block hides the real error', 'degrade gracefully when the payment provider is down', 'why does our retry storm take the database down', 'maneja los errores sin tragarte la excepción', 'afegeix reintents amb backoff i tallacircuits'. NOT finding why one specific thing broke (that is debug), NOT wiring logs/metrics/traces (that is observability), NOT the on-the-wire error envelope contract (that is api-design).",
1578
+ "description": "Use when designing the reaction to a class of failures — typed error taxonomies, retry/backoff/timeout policy, circuit breakers, React/Next error boundaries, and the user-message vs operator-log split. NOT diagnosing one specific crash (that is debug), NOT logs/metrics/traces (that is observability), NOT the wire error envelope (that is api-design).",
1579
1579
  "tags": [
1580
1580
  "error-handling",
1581
1581
  "resilience",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: db-migrations
3
- description: "Use when planning a schema change that must ship without downtime — adding a NOT NULL column, renaming/splitting/merging a column or table, changing a type, or backfilling millions of rows on a live table — and you need the expand-contract sequence plus the lock/timeout/batching discipline that keeps each step from freezing prod. Triggers: 'zero-downtime migration', 'add NOT NULL column to a huge table', 'rename a column safely', 'our deploy keeps deadlocking when we add an index', 'backfill 80M rows without replication lag', 'expand contract', 'gh-ost vs pt-osc', 'migración sin downtime', 'migració sense tallar el servei'. NOT Postgres lock internals/EXPLAIN/RLS (that is postgresdb), NOT drizzle-kit runner mechanics (that is drizzle-orm), NOT restore drills/PITR (that is backups)."
3
+ description: "Use when a schema change must ship without downtime — NOT NULL, rename, type change, or backfilling millions of live rows — for the expand-contract sequence and the lock/batching discipline that keeps each step from freezing prod. NOT lock internals or EXPLAIN (that is `postgresdb`), NOT drizzle-kit mechanics (that is `drizzle-orm`), NOT PITR (that is `backups`)."
4
4
  tags: [migrations, zero-downtime, expand-contract, schema, backfill, ddl]
5
5
  recommends: [postgresdb, drizzle-orm, backups, sql, planetscale]
6
6
  origin: risco
@@ -17,20 +17,9 @@ point without an outage.
17
17
 
18
18
  This skill is engine-neutral. It owns the *choreography* — what order to run things in and how to keep each
19
19
  DDL from freezing the table — across Postgres, MySQL, and serverless variants, whatever runner you use
20
- (Flyway, Alembic, golang-migrate, drizzle-kit, raw SQL). It does not teach one ORM's CLI and it is not a
21
- Postgres tuning manual.
20
+ (Flyway, Alembic, golang-migrate, drizzle-kit, raw SQL).
22
21
 
23
- ## When to use / When NOT to use
24
-
25
- **Use when:**
26
-
27
- - Sequencing a multi-step zero-downtime change: NOT NULL, rename, split/merge, type change, drop, add FK.
28
- - Backfilling a new column/table over millions of rows without blocking writes or blowing up replica lag.
29
- - Making one DDL statement safe: `lock_timeout`, `CONCURRENTLY`, running outside a transaction.
30
- - Coordinating deploy order so app version N and N+1 are both schema-compatible at every step.
31
- - Deciding rollback strategy (forward-fix vs reversible) and choosing an OSC tool for a big MySQL ALTER.
32
-
33
- **Route elsewhere:**
22
+ ## Route elsewhere
34
23
 
35
24
  - Postgres lock internals, EXPLAIN/ANALYZE, indexing strategy, RLS, PgBouncer → `../postgresdb/SKILL.md`.
36
25
  - Writing schema and running drizzle-kit specifically → `../drizzle-orm/SKILL.md`.
@@ -246,9 +235,3 @@ the same static checks against the example migrations shipped in `references/`.
246
235
  | Contracting before the cooling period | Kills your no-schema-change rollback path | wait until the new app has run clean; then drop |
247
236
  | Relying on `down()` in prod | Data already changed; rollback is unsafe or wrong | forward-only; expand-contract IS the rollback (Step 6) |
248
237
  | Coupling app + schema in one deploy | Breaks N/N-1; an instant where some app can't run | separate deploys; schema is a superset of both (Step 3) |
249
-
250
- ## References
251
-
252
- - `references/expand-contract-playbook.md` — ordered deploy/migrate checklists per change (NOT NULL, rename, split, type change, drop, add FK).
253
- - `references/backfill-and-batching.md` — batched backfill scripts (Postgres + MySQL), replica-lag throttle, checksum verification, snapshot+CDC for huge tables.
254
- - `references/tools-and-runners.md` — 2026 tool matrix, gh-ost vs pt-osc, per-runner transaction opt-out, Squawk CI config.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decision-records
3
- description: "Use when a consequential, hard-to-reverse choice was just made or is about to be — database, framework, auth model, vendor, API style, deployment target — and future-you will ask why, with the answer buried in chat; capture it as a numbered, immutable ADR with the context that forced the choice, the real options weighed with their trade-offs, the decision, and consequences both ways. Also when a prior decision is being reversed and must be superseded without erasing history, or when onboarding keeps re-litigating a settled choice. Triggers: 'write up why we picked Postgres over DynamoDB so we stop arguing', 'record the switch to tRPC but keep the old REST decision in history', 'documenta por qué elegimos Hetzner y qué alternativas descartamos con pros y contras', 'registra la decisió i les alternatives que vam descartar'. NOT the meeting recap with action items and owners (that is meeting-notes), NOT the project-wide standing principles and stack canon (that is constitution)."
3
+ description: "Use when a hard-to-reverse choice (database, framework, vendor, auth model) must be frozen as an immutable numbered ADR — the context that forced it, the options weighed, the decision, consequences both ways — or superseded without erasing history. NOT a meeting recap (that is meeting-notes), NOT standing project principles (that is constitution)."
4
4
  tags: [decision-records, adr, architecture-decisions, rationale, decision-log, madr, knowledge-ops]
5
5
  recommends: [meeting-notes, constitution, sop-builder, knowledge-ops, codebase-onboarding, plan, specify]
6
6
  origin: risco
@@ -182,7 +182,7 @@ A decision log is only useful if it stays navigable.
182
182
  - **Index every ADR at creation.** One row: `| 0007 | Choose Postgres | accepted | 2026-06-02 |`. No orphans.
183
183
  - **Keep supersession links live** in both directions.
184
184
  - **Review cadence.** Periodically sweep `accepted` ADRs — anything reality has overtaken gets a superseding record, not a silent edit.
185
- - **Index the log from the entry point** — `02-DOCS/wiki/index.md` (the Knowledge map; root `CLAUDE.md` keeps only a short pointer) — so onboarding finds it. The broader wiki and onboarding doc are owned by `knowledge-ops` and `codebase-onboarding`; this skill owns only the ADRs the wiki links to. The meeting that spawned a decision routes to [`meeting-notes`](../meeting-notes/SKILL.md); the repeatable how-to it implies routes to [`sop-builder`](../sop-builder/SKILL.md).
185
+ - **Index the log from the entry point** — `02-DOCS/wiki/index.md` — so onboarding finds it. The broader wiki and onboarding doc belong to [`knowledge-ops`](../knowledge-ops/SKILL.md) and [`codebase-onboarding`](../codebase-onboarding/SKILL.md); this skill owns only the ADRs the wiki links to. The meeting that spawned a decision routes to [`meeting-notes`](../meeting-notes/SKILL.md); the repeatable how-to it implies routes to [`sop-builder`](../sop-builder/SKILL.md).
186
186
 
187
187
  ## Anti-patterns
188
188
 
@@ -199,4 +199,4 @@ A decision log is only useful if it stays navigable.
199
199
 
200
200
  ## Verify
201
201
 
202
- Lint a produced ADR (or a whole decisions dir) with `scripts/verify.sh <path>`: it checks for a recognized status, a date, the required sections, ≥2 options, and a valid filename — read-only, no network. See [`references/templates.md`](references/templates.md) for the skeletons it expects.
202
+ Lint a produced ADR (or a whole decisions dir) with `scripts/verify.sh <path>`: it checks for a recognized status, a date, the required sections, ≥2 options, and a valid filename — read-only, no network. It expects the skeletons above.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: deep-learning
3
- description: "Use when training or debugging a neural network in PyTorch — writing the training loop, chasing loss that won't drop or goes NaN, adding mixed precision (AMP), scaling across GPUs, or making a run reproducible. Covers tensors/autograd/nn.Module, torch.compile, the forward→loss→backward→step→zero_grad loop and its silent bugs, AdamW + cosine/warmup schedulers, DDP vs FSDP vs DeepSpeed ZeRO, checkpointing and seeds. PyTorch-first (JAX/TF exist). Triggers: 'my training loss isn't decreasing', 'loss becomes NaN after a few steps', 'how do I use torch.autocast and GradScaler', 'train this model across 4 GPUs', 'val accuracy is worse than train and I forgot model.eval()', 'gradients seem to add up across batches', 'build a CNN from scratch and train it', 'entrenar una xarxa neuronal des de zero amb PyTorch'. NOT adapting a pretrained LLM with LoRA/QLoRA (that is finetuning); NOT classic/tabular models like XGBoost or sklearn (that is machine-learning); NOT tokenization or NLP task metrics (that is nlp)."
3
+ description: "Use when training or debugging a neural net in PyTorch — the forward/loss/backward/step loop and its silent bugs, mixed precision (AMP), AdamW/LR schedules, DDP/FSDP/ZeRO, checkpoints and seeds. NOT LoRA/QLoRA on a pretrained LLM (that is `finetuning`), NOT tabular sklearn/XGBoost (that is `machine-learning`), NOT tokenization or NLP metrics (that is `nlp`)."
4
4
  tags: [pytorch, deep-learning, neural-networks, training-loop, autograd, mixed-precision, amp, ddp, fsdp, optimizer, lr-scheduler, reproducibility, torch-compile]
5
5
  recommends: [machine-learning, finetuning, nlp, python]
6
6
  origin: risco
@@ -27,8 +27,7 @@ is stable 2.x API. The one namespace shift to know: AMP now lives under `torch.a
27
27
  | Adapting a *pretrained LLM* — LoRA/QLoRA, SFT, trl/peft | [`finetuning`](../finetuning/SKILL.md) |
28
28
  | Classic/tabular — sklearn, XGBoost/LightGBM, feature engineering | [`machine-learning`](../machine-learning/SKILL.md) |
29
29
  | Tokenization, NLP task modeling, task metrics (F1/BLEU/ROUGE) | [`nlp`](../nlp/SKILL.md) |
30
-
31
- If the model is a from-scratch or vision/custom net and you're writing the loop yourself, stay.
30
+ | Envs, packaging, tests and hygiene around the model code | [`python`](../python/SKILL.md) |
32
31
 
33
32
  ## 1. PyTorch essentials
34
33
 
@@ -83,7 +82,7 @@ for epoch in range(epochs):
83
82
  scheduler.step() # if a per-step scheduler
84
83
  ```
85
84
 
86
- The load-bearing traps (all in the gotchas table, worth stating once here):
85
+ The load-bearing traps, stated next to the lines they govern:
87
86
 
88
87
  - **Missing `optimizer.zero_grad()` → gradients silently add up across batches.** PyTorch
89
88
  *accumulates* `.grad` by design (so you can split a batch); if you never zero it, every step
@@ -200,7 +199,6 @@ Save optimizer + scheduler + scaler too, or a resumed run restarts its LR schedu
200
199
  `pytorch.org/docs/stable/notes/randomness.html`, quoted: *"Completely reproducible results are not
201
200
  guaranteed across PyTorch releases, individual commits, or different platforms. Furthermore,
202
201
  results may not be reproducible between CPU and GPU executions, even when using identical seeds."*
203
- So bit-exact reproducibility is a debugging/regression tool, not a guarantee.
204
202
 
205
203
  ```python
206
204
  import random, numpy as np, torch
@@ -217,47 +215,12 @@ DataLoader workers need their own seeding (each worker forks its RNG), via `work
217
215
  a seeded `generator` — full snippet in `references/training-recipes.md`. Determinism costs speed
218
216
  (`benchmark=False` disables autotuned convs).
219
217
 
220
- ## Guardrails (the silent killers)
221
-
222
- - **`optimizer.zero_grad()` every step.** Skipping it makes `.grad` accumulate across batches — no
223
- error, just diverging training. (`torch.optim` / optimization tutorial, pytorch.org)
224
- - **`model.eval()` + `torch.no_grad()` at inference/validation.** Forgetting `eval()` leaves Dropout
225
- dropping and BatchNorm using *batch* stats instead of running stats, so val numbers are wrong;
226
- forgetting `no_grad()` builds the autograd graph and can OOM. Flip back to `model.train()` after.
227
- (`nn.Module.eval` / autograd docs, pytorch.org)
228
- - **Determinism is not guaranteed across releases/platforms/CPU-vs-GPU** even with identical seeds —
229
- seed for debugging, don't promise bit-exactness. (randomness note, pytorch.org)
230
- - **fp16 without `GradScaler`** underflows to zero grads → no learning. bf16 doesn't need it.
231
- - **`CrossEntropyLoss`/`BCEWithLogitsLoss` take raw logits** — no Softmax/Sigmoid before them.
232
- - **Everything on the same device** — a stray CPU tensor in a CUDA graph is a hard error you'll hit
233
- fast; `.to(device)` inputs, targets, and model.
234
- - **NaN loss?** LR too high, no AMP scaler on fp16, unclipped exploding grads, or a `log(0)`/`0/0`
235
- in a custom loss. Bisect: drop LR 10×, clip grads, switch fp16→bf16.
236
-
237
- ## Related skills
238
-
239
- - [`finetuning`](../finetuning/SKILL.md) — adapting a **pretrained LLM** (LoRA/QLoRA, SFT via
240
- trl/peft). If the base is a foundation model you're specializing, that's the skill; this one is
241
- general/from-scratch net training.
242
- - [`machine-learning`](../machine-learning/SKILL.md) — classic/**tabular** (sklearn, GBDT). No
243
- autograd, no GPU loop.
244
- - [`nlp`](../nlp/SKILL.md) — tokenization, NLP **task** modeling and metrics. Route text task
245
- specifics there; route the raw training loop back here.
246
- - [`python`](../python/SKILL.md) — env/packaging, testing, general Python hygiene under the model
247
- code.
248
-
249
- ## Checklist
250
-
251
- - [ ] Right skill: from-scratch/vision/custom net in PyTorch (not finetuning / ML / nlp).
252
- - [ ] Loop has all five steps in order; `zero_grad(set_to_none=True)` every step.
253
- - [ ] Loss takes raw logits (no double activation); running loss uses `.item()`.
254
- - [ ] `model.train()` for training; `model.eval()` **and** `torch.no_grad()` for val/inference.
255
- - [ ] AMP: `torch.autocast` on forward+loss only; fp16 → `GradScaler`, bf16 → skip it; clip after
256
- `unscale_`.
257
- - [ ] AdamW (decoupled decay) with no weight decay on bias/Norm params; LR warmup→cosine, `step()`
258
- cadence matches scheduler units.
259
- - [ ] Multi-GPU chosen by fit: DDP (fits) vs FSDP2 (doesn't) vs ZeRO (offload/config).
260
- - [ ] Checkpoint saves model+optimizer+scheduler+scaler `state_dict`s; resume loads model first.
261
- - [ ] Seeds set; determinism flags on if needed, with the "not guaranteed across releases/devices"
262
- caveat acknowledged.
263
- - [ ] Version claims hedged to "PyTorch 2.x (~2.13, verify)"; no hard minor pin presented as current.
218
+ ## Anti-patterns (the silent killers)
219
+
220
+ | Anti-pattern | What actually happens | Instead |
221
+ |---|---|---|
222
+ | Validating with the model left in `train()` mode, or without `torch.no_grad()` | Dropout keeps dropping and BatchNorm uses *batch* stats instead of running stats, so val numbers are wrong; the graph is still built and can OOM | `model.eval()` + `torch.no_grad()` around val/inference, `model.train()` after (`nn.Module.eval` / autograd docs, pytorch.org) |
223
+ | Reading a NaN loss as a modeling problem | It is almost always LR too high, fp16 with no `GradScaler`, unclipped exploding grads, or a `log(0)`/`0/0` in a custom loss | Bisect: drop LR 10×, clip grads, switch fp16→bf16 |
224
+ | fp16 with the scaler left off | Gradients underflow to zero → no learning, and no error to tell you | Keep `GradScaler` on fp16, or move to bf16 where underflow can't happen |
225
+ | A stray CPU tensor in a CUDA graph | Hard device-mismatch error mid-step, usually on the targets or a hand-built mask rather than the inputs | `.to(device)` inputs, targets **and** model |
226
+ | Promising bit-exact reproducibility from seeds | Not guaranteed across releases, commits, platforms, or CPU-vs-GPU even with identical seeds | Seed for debugging/regression and say so; state the caveat with the result (randomness note, pytorch.org) |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: digitalocean
3
- description: "Use when deploying or operating a workload on DigitalOcean and you must decide where it runs and how to ship it — Droplet (raw VPS) vs App Platform (managed PaaS) vs Functions, wiring a Managed Database (Postgres/MySQL/Valkey), Spaces object storage + CDN, doctl auth, App Platform app spec YAML, cloud firewalls, VPC private networking, reserved IPs, snapshots/backups, app logs, scaling. Triggers: 'deploy to DigitalOcean', 'DigitalOcean App Platform app spec', 'DO managed postgres with connection pooling', 'doctl apps create', 'reserved IP failover', 'I need an S3-compatible bucket with a CDN' (that is Spaces — non-obvious), 'subir mi web a DigitalOcean con doctl', 'Droplet o App Platform, qué sale más barato'. NOT host-agnostic CI/CD or rollback strategy (that is deployment), NOT a Hetzner bare VPS (that is hetzner)."
3
+ description: "Use when deploying or operating a workload on DigitalOcean — Droplet vs App Platform vs Functions, doctl, app spec YAML, Managed Postgres/MySQL/Valkey on the VPC, S3-compatible Spaces + CDN. NOT host-agnostic CI/CD or rollback strategy (that is `deployment`), NOT a bare Hetzner VPS (that is `hetzner`), NOT another managed PaaS (that is `railway`)."
4
4
  tags: [digitalocean, doctl, app-platform, droplets, spaces, deployment, paas, vps]
5
5
  recommends: [deployment, postgresdb, docker, domains-dns, monitoring, scaling, hetzner]
6
6
  origin: risco
@@ -8,10 +8,11 @@ origin: risco
8
8
 
9
9
  # DigitalOcean — Droplet vs App Platform, doctl, Managed DBs, Spaces
10
10
 
11
- You own one decision: **where on DO does this run, and how do I ship it.** Everything
12
- else (CI/CD shape, Dockerfile, DB schema, DNS) belongs to a sibling — see the boundary at
13
- the bottom. Pick the compute model first, then wire data and storage, then the ops that
14
- bite in production.
11
+ You own one decision: **where on DO does this run, and how do I ship it.** Host-agnostic
12
+ CI/CD, release gating and rollback strategy are `../deployment/SKILL.md`; authoring the
13
+ Dockerfile is `docker`; DNS records and the registrar are `domains-dns`; a bare Hetzner box
14
+ and its economics are `hetzner`. Pick the compute model first, then wire data and storage,
15
+ then the ops that bite in production.
15
16
 
16
17
  ```text
17
18
  workload → [Droplet | App Platform | Functions] → Managed DB (VPC private host) → Spaces (S3 + CDN)
@@ -34,11 +35,9 @@ Droplet only when you need the box itself.
34
35
  Rules of thumb:
35
36
 
36
37
  - **Stateless web service or API from a Git repo → App Platform.** It builds, deploys, gives
37
- you TLS and a URL, and re-deploys on push. No box to patch.
38
+ you TLS and a URL, and re-deploys on push. No box to patch. **Static frontend → App
39
+ Platform static site (free, up to 3)**; don't run a Droplet to serve HTML.
38
40
  - **You need root, a long-lived stateful daemon, custom ports, or a cron host → Droplet.**
39
- App Platform components are not meant to be a persistent pet process.
40
- - **Static frontend → App Platform static site (free, up to 3).** Don't run a Droplet to
41
- serve HTML.
42
41
  - **A managed Postgres/MySQL/Valkey → always the Managed Database product**, never a DB you
43
42
  hand-install on a Droplet, unless you have a hard reason.
44
43
 
@@ -156,8 +155,8 @@ doctl compute droplet create web-1 \
156
155
  to spin up and destroy, but a powered-off Droplet still bills for storage — destroy, don't
157
156
  just power off, to stop charges.
158
157
 
159
- Cloud-init recipes, firewall inbound/outbound rule sets, snapshot cadence, and reserved-IP
160
- failover live in `references/droplet-ops.md`.
158
+ Cloud-init recipes, firewall inbound/outbound rule sets, snapshot cadence + restore,
159
+ reserved-IP failover, and the VPC + private-DB layout live in `references/droplet-ops.md`.
161
160
 
162
161
  ## Managed Databases
163
162
 
@@ -206,11 +205,6 @@ browser fetches it cross-origin, and a **lifecycle** rule to expire/transition o
206
205
 
207
206
  ## Basic ops + cost gotchas
208
207
 
209
- ```bash
210
- doctl apps logs <app-id> --type run --follow # app logs
211
- doctl apps update <app-id> --spec .do/app.yaml # change instance_count/size by editing the spec
212
- ```
213
-
214
208
  - Set **alerts** on the app/DB (deploy failures, CPU, restart count) so you hear about
215
209
  trouble before users do. App-level dashboards/alerting *strategy* is `../monitoring`.
216
210
  - **Scale** by editing `instance_count`/`instance_size_slug` in the spec and re-applying;
@@ -231,21 +225,3 @@ doctl apps update <app-id> --spec .do/app.yaml # change instance_count/size by e
231
225
  | Hand-installing Postgres on a Droplet "to save money" | You now own patching, backups, failover | Managed Database unless you have a hard reason |
232
226
  | Powering off a Droplet to stop billing | Powered-off Droplets still bill for storage | Snapshot then destroy |
233
227
  | Leaving a reserved IP unassigned | It's billed when not attached | Release it |
234
-
235
- ## References
236
-
237
- - `references/app-spec.md` — full multi-component app spec (web + worker + static_site + job
238
- + database), health checks, ingress routes, instance-size slug table, env/SECRET scoping,
239
- autoscaling and alerts.
240
- - `references/droplet-ops.md` — cloud-init examples, cloud firewall inbound/outbound rule
241
- recipes, snapshot/backup cadence + restore, reserved IP failover, VPC + private DB layout.
242
-
243
- ## Boundary
244
-
245
- This skill is DigitalOcean-specific deployment. For neighbors:
246
-
247
- - Host-agnostic CI/CD pipelines, release gating, rollback strategy → `../deployment/SKILL.md`.
248
- - Postgres schema/query/index work (not "provision a DO cluster") → `../postgresdb/SKILL.md`.
249
- - Authoring the Dockerfile itself → `docker`. DNS records/registrar → `domains-dns`.
250
- - Uptime/metrics dashboards + alerting strategy → `monitoring`. Host-agnostic capacity →
251
- `scaling`. A Hetzner bare VPS and its economics → `hetzner`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: django
3
- description: "Use when building, reviewing, securing, testing or shipping a Django web app — models, migrations, QuerySets, managers, FBV/CBV views, forms, the admin, settings split, and Django REST Framework (serializers, ModelViewSet, routers, permissions, throttling). Triggers: 'add a filter and fix the N+1 in this ListView', 'build a DRF ModelViewSet with owner-only permission', 'this template loops over books and queries book.author each iteration' (N+1 without the word Django), 'why does manage.py check --deploy warn about SECURE_HSTS_SECONDS', 'revisa este modelo y añade un UniqueConstraint en (tenant, slug)', 'el queryset hace N+1', any file with manage.py, settings.py, models.py, serializers.py or a 0001_initial migration. NOT async-first FastAPI services (that is fastapi)."
3
+ description: "Use when building, reviewing, securing, testing or shipping a Django app — models, migrations, QuerySets/managers, FBV/CBV views, forms, the admin, settings split, and Django REST Framework (serializers, ModelViewSet, permissions). NOT async FastAPI/Pydantic services (that is `fastapi`), NOT Postgres schema/index work (that is `postgresdb`)."
4
4
  tags: [python, django, orm, drf, backend, web]
5
5
  recommends: [postgresdb, secure-coding, deployment, testing-py, api-design]
6
6
  origin: risco
@@ -31,10 +31,7 @@ add a dependency.
31
31
 
32
32
  **Version rule:** default to 5.2 LTS. Pick 6.0 only for a concrete Tasks/CSP need, and say so.
33
33
 
34
- ## When to use vs route elsewhere
35
-
36
- The test: if the project has `manage.py` and `INSTALLED_APPS`, it is Django. If it imports
37
- `fastapi` + `pydantic`, it is not.
34
+ ## Route elsewhere
38
35
 
39
36
  | Situation | Route to |
40
37
  |---|---|
@@ -45,8 +42,6 @@ The test: if the project has `manage.py` and `INSTALLED_APPS`, it is Django. If
45
42
  | REST contract design (cursor vs offset, status codes, versioning) | `api-design` |
46
43
  | ruff/uv/general type hints, packaging | `python` |
47
44
 
48
- Everything else Django-shaped — models, QuerySets, migrations, views, DRF, settings — is here.
49
-
50
45
  ## Project shape
51
46
 
52
47
  Split settings by environment; never ship one `settings.py` toggled by `DEBUG`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: docker
3
- description: "Use when authoring or auditing a Dockerfile/Containerfile, shrinking a bloated image, hardening a container that runs as root, choosing a base image, or wiring a Compose dev loop with hot reload. Triggers: 'dockerize this app', 'my node image is 1.2GB make it smaller', 'the build cache keeps busting', 'harden this Dockerfile, trivy flags it', 'docker compose watch for local dev', 'distroless vs alpine vs slim', 'la imagen pesa demasiado', 'el build de docker tarda siglos'. NOT the CI/CD pipeline or deploy-to-host (that is deployment), NOT k8s autoscaling/HPA (that is scaling), NOT app-level SQL injection or secrets-in-code (that is secure-coding)."
3
+ description: "Use when authoring or auditing a Dockerfile, shrinking a bloated image, hardening a container that runs as root, picking a base image, or wiring a Compose dev loop with hot reload. NOT CI builds or deploy-to-host (that is `deployment`), NOT k8s autoscaling (that is `scaling`), NOT app-level injection or secrets-in-code (that is `secure-coding`)."
4
4
  tags: [docker, containers, dockerfile, compose, image-security]
5
5
  recommends: [deployment, secure-coding, scaling, github-actions]
6
6
  origin: risco
@@ -20,17 +20,8 @@ source → Dockerfile (multi-stage, cache mounts) → small + scanned image
20
20
  ```
21
21
 
22
22
  The hand-off is sharp: **the moment you have a green, scanned image and a working
23
- `compose.yaml`, you are done.** Pushing to a registry, building in CI, deploying to a host,
24
- rollback — that is `../deployment/SKILL.md`. It already references Dockerfiles; you are the
25
- image-internals specialist it leans on.
26
-
27
- ## When to use
28
-
29
- - Authoring or auditing a `Dockerfile` for any stack (Node, Python, Go, Rust, JVM, static SPA).
30
- - An image is too big (800 MB+ Node image), builds too slowly, or fails `trivy`/`hadolint`.
31
- - A container runs as root and you need least privilege.
32
- - Standing up local dev with `compose.yaml` + `docker compose watch` (sync / rebuild / sync+restart).
33
- - Choosing a base image and getting multi-stage + BuildKit cache mounts right.
23
+ `compose.yaml`, you are done.** Everything downstream is `../deployment/SKILL.md` — it already
24
+ references Dockerfiles and leans on you for the image internals.
34
25
 
35
26
  ## When NOT to use — and who owns it instead
36
27
 
@@ -42,24 +33,6 @@ image-internals specialist it leans on.
42
33
  | Install/operate a self-hosted PaaS | `../coolify/SKILL.md` | Host operation, not the image |
43
34
  | Containerless PaaS deploy mechanics | `../railway/SKILL.md`, `../fly-io/SKILL.md` | Platform deploy, not the Dockerfile |
44
35
 
45
- ## The five rules
46
-
47
- 1. **Multi-stage, always.** Build deps (compilers, dev packages, the full SDK) must never reach
48
- the runtime image. Multiple `FROM` stages; copy only the built artifact into a minimal final
49
- stage. *Why: a Go binary needs ~10 MB, not a 900 MB toolchain — and every extra package is CVE
50
- surface.*
51
- 2. **Choose the base deliberately and pin it — never `:latest`.** Pin a tag (ideally a digest)
52
- so a rebuild is reproducible. *Why: `:latest` silently changes under you, breaking builds and
53
- busting the scan you just passed.*
54
- 3. **Install deps before copying source.** Order layers so the lockfile install is cached and use
55
- BuildKit cache mounts. *Why: copying everything first invalidates the dependency layer on every
56
- one-character source edit.*
57
- 4. **Run as a non-root `USER`.** Default PID 1 is root (UID 0); a container escape from root is a
58
- host compromise. *Why: least privilege is the cheapest blast-radius reduction you can ship.*
59
- 5. **One concern per container; EXEC-form `CMD`; add a `HEALTHCHECK`.** *Why: shell-form `CMD foo`
60
- wraps your process in `/bin/sh -c`, which swallows `SIGTERM` so the container ignores graceful
61
- shutdown; a healthcheck lets Compose/orchestrators gate on readiness.*
62
-
63
36
  ## Pick a base image (2026 reality)
64
37
 
65
38
  | Base | Size | CVE / patch velocity | Debuggable? | Wins when |
@@ -72,7 +45,8 @@ image-internals specialist it leans on.
72
45
 
73
46
  Rule of thumb: glibc app with native deps → `*-slim` or Wolfi. Static Go binary → `scratch` or
74
47
  `distroless/static`. Security mandate → Chainguard. Reach for `alpine` only when you've confirmed
75
- your wheels/native libs build against musl. Per-language tag maps live in
48
+ your wheels/native libs build against musl. Per-language tag maps, multi-stage templates for the
49
+ stacks not shown below (Rust, JVM `jlink`, static SPA → nginx) and multi-arch buildx live in
76
50
  [references/base-images-and-stages.md](references/base-images-and-stages.md).
77
51
 
78
52
  ## Multi-stage skeletons
@@ -204,8 +178,9 @@ docker build --secret id=npm_token,env=NPM_TOKEN .
204
178
  ## Compose for dev
205
179
 
206
180
  Canonical filename is `compose.yaml`; **omit the obsolete `version:` key**; use `docker compose`
207
- (v2 subcommand), never the standalone `docker-compose` v1. `develop.watch` is GA since Compose
208
- 2.22.0 and gives you a tight dev loop.
181
+ (v2 subcommand), never the standalone `docker-compose` v1. One concern per container — the app, the
182
+ database and the seed job are separate services. `develop.watch` is GA since Compose 2.22.0 and
183
+ gives you a tight dev loop.
209
184
 
210
185
  ```yaml
211
186
  services:
@@ -264,20 +239,13 @@ These are exactly what `scripts/verify.sh` automates (it skips a tool gracefully
264
239
 
265
240
  | Bad | Good | Why |
266
241
  |---|---|---|
267
- | `FROM node:latest` | `FROM node:24-bookworm-slim` (pin/digest) | `:latest` drifts; builds + scans become non-reproducible |
268
- | runs as root (no `USER`) | `USER nonroot` / `USER 10001` | root escape == host compromise |
242
+ | `FROM node:latest` | `FROM node:24-bookworm-slim`, or pin a digest | `:latest` drifts under you; builds + scans become non-reproducible |
243
+ | runs as root (no `USER`) | `USER nonroot` / `USER 10001` | PID 1 defaults to root (UID 0), so a container escape == host compromise |
269
244
  | `ARG TOKEN` / `ENV SECRET=` | `RUN --mount=type=secret,...` | ARG/ENV persist in image history & layers |
270
245
  | `COPY . .` then `RUN npm ci` | copy lockfile → install → copy source | source edits bust the dep cache layer every build |
271
246
  | single `FROM`, ship the SDK | multi-stage, copy only the artifact | runtime carries compilers & dev CVE surface |
272
247
  | `apt-get install x` | `apt-get install --no-install-recommends x && rm -rf /var/lib/apt/lists/*` | recommends + apt lists bloat the layer |
273
248
  | no `.dockerignore` | mirror `.gitignore` + build outputs | whole context ships; `.env`/`.git` can leak |
274
249
  | `version: "3.8"` / `docker-compose` v1 | omit `version:`; `docker compose` v2 | the key is obsolete; v1 is EOL |
275
- | `CMD npm start` (shell form) | `CMD ["node", "server.js"]` (exec) | shell form swallows `SIGTERM`; no graceful shutdown |
276
-
277
- ## References & siblings
278
-
279
- - [references/base-images-and-stages.md](references/base-images-and-stages.md) — per-language
280
- multi-stage templates (Rust, JVM jlink, static SPA → nginx), tag maps, multi-arch buildx.
281
- - [references/compose-recipes.md](references/compose-recipes.md) — fuller Compose patterns.
282
- - Once the image is green and scanned: `../deployment/SKILL.md`. Image/container hardening pairs
283
- with `../secure-coding/SKILL.md`. Self-hosted host ops: `../coolify/SKILL.md`.
250
+ | `CMD npm start` (shell form) | `CMD ["node", "server.js"]` (exec) | shell form wraps the process in `/bin/sh -c`, which swallows `SIGTERM` — no graceful shutdown |
251
+ | no `HEALTHCHECK` | `HEALTHCHECK CMD [...]` | nothing for Compose/orchestrators to gate readiness on (`condition: service_healthy`) |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: document-processing
3
- description: "Use when you need to get content out of a document or build a document out of data — extracting text/tables from PDFs, filling interactive PDF forms (AcroForm), generating PDF/DOCX from templates, or OCR'ing scanned/image-only files into Markdown/JSON. Triggers: 'extract the line items from this invoice PDF', 'fill this PDF form and flatten it', 'generate a contract DOCX from this template', 'this scan has no text layer', 'the PDF copies out as garbage symbols', 'merge/split/rotate these PDFs', 'OCR 300 scanned pages', 'rellena este formulario PDF y fusiónalos', 'extreu les taules d'aquesta factura escanejada'. NOT pulling schema-typed fields out of text (that is structured-extraction), NOT sending it for signature (that is e-signature), NOT reading spreadsheet cells/formulas (that is spreadsheet-ops)."
3
+ description: "Use when the deliverable is a document's bytes or its literal content — text/tables out of PDFs, AcroForm fill and flatten, page merge/split, PDF/DOCX from templates, OCR of image-only scans. NOT schema-typed fields pulled from text (that is structured-extraction), NOT signature routing (e-signature) or spreadsheet cells/formulas (spreadsheet-ops)."
4
4
  tags: [pdf, ocr, docx, forms, extraction, document-ai]
5
5
  recommends: [structured-extraction, e-signature, spreadsheet-ops, rag, data-scraper]
6
6
  origin: risco
@@ -15,7 +15,7 @@ The boundary test, apply it first:
15
15
  - Deliverable is **raw text / Markdown / table cells / a generated file** → you are in the right place.
16
16
  - Deliverable is **a typed object matching a schema** (`{parties: [...], total: 1234.50}`) → that is `structured-extraction`. This skill stops at "clean Markdown out of the file"; the schema-constrained extraction runs on that Markdown.
17
17
 
18
- Everything else routes too: signing → `e-signature`, spreadsheet grids/formulas → `spreadsheet-ops`, indexing for Q&A → `rag`, downloading the files off a site → `data-scraper`.
18
+ Everything else routes too: signing with an audit trail → `e-signature`, spreadsheet grids/formulas/XLSX-as-data → `spreadsheet-ops`, indexing for cross-document Q&A → `rag` (this skill *produces* the text `rag` ingests, it does not index it), downloading the files off a site → `data-scraper`.
19
19
 
20
20
  ## Step 0 — does the PDF have a text layer?
21
21
 
@@ -42,7 +42,7 @@ If `extract_text()` returns empty (or near-empty) across the first few pages, it
42
42
  | Goal | Use | Why |
43
43
  |---|---|---|
44
44
  | Extract text + tables with layout | **pdfplumber** | Layout-aware; `extract_tables()` returns rows/cols as Python lists → pandas/CSV. |
45
- | Raw text, merge, split, rotate, page ops | **pypdf** (6.12.2) | Pure-Python, no C deps, runs in Lambda/containers; the maintained core. |
45
+ | Raw text, merge, split, rotate, page ops | **pypdf** (6.12.2) | Pure-Python, no C deps, runs in Lambda/containers; the maintained core — import `pypdf`, never the dead `PyPDF2`, which was merged back into it. |
46
46
  | Fill an interactive PDF form | **pypdf** | `update_page_form_field_values` writes AcroForm fields; can flatten. |
47
47
  | Generate a Word/DOCX from a template | **docxtpl** (0.20.x) | A real `.docx` becomes a Jinja2 template; author in Word, tag, render. |
48
48
  | Generate a PDF from scratch | **ReportLab** | Canvas / Platypus flowables for laid-out PDFs. |
@@ -50,8 +50,6 @@ If `extract_text()` returns empty (or near-empty) across the first few pages, it
50
50
  | OCR messy scans / handwriting / hard tables, API ok | **Mistral OCR** | `mistral-ocr-2512` (OCR 3), ~$2 / 1,000 pages, tuned for forms + handwriting. |
51
51
  | Fastest extract / easiest page→PNG raster | **PyMuPDF** ⚠️ **AGPL** | Fast, but AGPL: shipping it imposes an open-source obligation or needs a paid license. Flag this before recommending. |
52
52
 
53
- Rule: use the maintained **`pypdf`** import, never the dead `PyPDF2` — `PyPDF2` is unmaintained and was merged back into `pypdf`. Importing it is a signal of stale code.
54
-
55
53
  ## Extraction recipes
56
54
 
57
55
  Text + tables with `pdfplumber`, straight to CSV:
@@ -128,7 +126,7 @@ with open("filled.pdf", "wb") as f:
128
126
  writer.write(f)
129
127
  ```
130
128
 
131
- Why `auto_regenerate=False`: it defaults to `True` for legacy reasons, which marks the AcroForm dirty and triggers a "you have unsaved changes" prompt when the user opens the PDF. You almost never want that. Checkbox/radio values are the field's `/V` on-state (often `/Yes`), not `True` — read the field to find it.
129
+ `auto_regenerate` defaults to `True` for legacy reasons, and you almost never want it. Checkbox/radio values are the field's `/V` on-state (often `/Yes`), not `True` — read the field to find it.
132
130
 
133
131
  ## Generation
134
132
 
@@ -191,16 +189,10 @@ markdown = "\n\n".join(p.markdown for p in resp.pages)
191
189
 
192
190
  **Never trust OCR output blind.** OCR confuses `0/O`, `1/l/I`, and drops or shifts decimal points — a `1.234,50` can come back as `1234,50` or `1,234.50`. Always spot-check totals, dates, and ID numbers against the rendered page before you hand the text downstream. For clean scans with no budget, plain `pytesseract` is the zero-cost baseline, but it is weak on layout/tables versus the pipelines above.
193
191
 
194
- ## Scale and handoff
192
+ ## Scale
195
193
 
196
194
  Batch jobs: parallelize per-file, cap concurrency on the hosted API (rate limits + cost), and use Mistral's Batch API for the 50% discount on large runs. Engine install matrix, exact version pins, the full licensing table, the Docling-vs-Marker-vs-Mistral feature/cost comparison, and troubleshooting (encrypted PDFs, mangled AcroForm field names, multi-column reading order, CJK/handwriting) live in `references/engines.md` — read it before a non-trivial install.
197
195
 
198
- Handoffs:
199
- - Need typed fields (`{total, due_date, parties}`) out of the Markdown you produced → `structured-extraction`.
200
- - Need to route the finished PDF for signature with an audit trail → `e-signature`.
201
- - The grid is really a spreadsheet (cells, formulas, XLSX as data) → `spreadsheet-ops`.
202
- - Need to index the extracted text for cross-document Q&A → `rag`. This skill *produces* the text `rag` ingests; it does not index it.
203
-
204
196
  ## Anti-patterns
205
197
 
206
198
  | Anti-pattern | Why it is wrong | Do instead |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: e2e-testing
3
- description: "Use when writing or stabilizing browser end-to-end tests with Playwright — driving real navigation, forms, logins, and multi-step user journeys through an actual browser, picking durable locators, or killing tests that pass locally but flake in CI. Covers getByRole/getByTestId locator choice, web-first assertions, storageState auth via setup projects, retries/trace config, sharding, and the 2026 flakiness playbook. Triggers: 'write a Playwright test', 'my e2e tests flake in CI but pass locally', 'set up storageState so I don't log in every test', 'getByRole vs CSS selector', 'shard Playwright across machines', 'trace on retry', 'strict mode resolved to 2 elements', 'els tests d'extrem a extrem fallen aleatòriament al pipeline', 'los tests e2e fallan a veces'. NOT in-process component/unit web tests (that is testing-web), NOT pytest suites (that is testing-py), NOT a11y audits as the goal (that is accessibility), NOT building the CI pipeline itself (that is github-actions)."
3
+ description: "Use when writing or stabilizing Playwright tests that drive a real browser through multi-step journeys — durable locators, web-first assertions, storageState auth, trace/retries, and flakes that only bite in CI. NOT in-process component tests (that is testing-web), NOT WCAG auditing (that is accessibility), NOT the pre-merge gate (that is verify)."
4
4
  tags: [playwright, e2e, browser-testing, flakiness, ci]
5
5
  recommends: [testing-web, accessibility, performance, github-actions, debug]
6
6
  origin: risco
@@ -18,17 +18,6 @@ line is **Playwright v1.60.x** (v1.60.0 shipped 2026-05-11). The `_react` / `_vu
18
18
  and the `:light` Shadow-DOM suffix were **removed in v1.58.0** — at any version you should be pinning
19
19
  they are long gone, so do not reach for them.
20
20
 
21
- ## What good looks like
22
-
23
- - **User-facing locators.** Tests find elements the way a user does — by role and accessible name —
24
- not by `div.card > button:nth-child(2)`. The test survives a refactor that the user never sees.
25
- - **Web-first assertions.** Every assertion auto-retries until the DOM settles. You never read a
26
- value once and compare it.
27
- - **Deterministic by design.** No `waitForTimeout`. No test that depends on another test's leftovers.
28
- Flakiness is a design defect you prevent, not a rerun you tolerate.
29
- - **Traces on retry.** CI captures a full trace the first time a test retries, so a CI-only failure
30
- is debuggable from the artifact without a local repro.
31
-
32
21
  ## Is this even an e2e test?
33
22
 
34
23
  E2e is the most expensive layer. Spend it only on journeys that cross pages or services. Route the rest out.
@@ -40,6 +29,7 @@ E2e is the most expensive layer. Spend it only on journeys that cross pages or s
40
29
  | "Is this page accessible?" — WCAG/ARIA as the deliverable | `../accessibility/SKILL.md` | E2e may *call* axe inside a test, but auditing a11y is its own skill. |
41
30
  | "Is this page fast?" — LCP/CWV budgets | `../performance/SKILL.md` | Perf budgets are a different signal than journey correctness. |
42
31
  | The runner matrix, caching, the pipeline itself | `../github-actions/SKILL.md` | E2e contributes a *job*; owning the pipeline is theirs. |
32
+ | "Is the change done?" — run the gate, collect evidence, then merge | `../verify/SKILL.md` | Running an existing suite as a pre-merge gate is not authoring or stabilizing one. |
43
33
 
44
34
  Rule: if you can prove it without launching a browser, you should. Push logic down to `testing-web`.
45
35
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: email-connector
3
- description: "Use when wiring an app to send transactional or bulk email through an HTTP provider (Resend, Twilio SendGrid, Postmark) from server code: welcome/reset/receipt sends, a provider-agnostic sendEmail() seam, React Email or dynamic templates, idempotent retries, 100-cap batch with partial-failure handling, transactional-vs-broadcast stream split, and the bounce/complaint webhook feeding a suppression list. Triggers: 'send a password-reset email with Resend', 'emails double-send when the queue retries', 'switch us off SendGrid onto Postmark without rewriting every call site', 'send a digest to 4000 users and handle the ones that fail', 'el webhook de bounces no actualiza la lista de supresión', 'montar el envío de correos con Resend'. NOT writing the subject line or the open/click growth system (that is newsletter)."
3
+ description: "Use when wiring server code to send transactional or bulk email via Resend, SendGrid, or Postmark: a provider-agnostic sendEmail() seam, idempotent retries, 100-cap batches with partial failures, transactional-vs-broadcast streams, bounce webhooks feeding a suppression list. NOT SPF/DKIM/DMARC inbox reputation (that is `email-deliverability`)."
4
4
  tags: [email, transactional-email, resend, sendgrid, postmark, webhooks, idempotency]
5
5
  recommends: [email-deliverability, newsletter, webhooks, secure-coding, api-connector-builder, automation-flows]
6
6
  origin: risco
@@ -13,30 +13,16 @@ digest — your job is the server code that hands it to a provider, makes it saf
13
13
  to retry, and keeps the suppression list honest. You do **not** own the inbox
14
14
  (SPF/DKIM/DMARC/reputation is `../email-deliverability/SKILL.md`) and you do
15
15
  **not** own the words (subject lines and growth are `../newsletter/SKILL.md`,
16
- launch copy is `../marketing/SKILL.md`). The deliverable is idempotent,
17
- secret-safe, stream-correct server code — not subject lines, DNS records, or
18
- audience strategy.
16
+ launch copy is `../marketing/SKILL.md`). Generic typed clients for *any* REST API
17
+ are `../api-connector-builder/SKILL.md`; deciding *when* a multi-step sequence
18
+ fires is `../automation-flows/SKILL.md`.
19
19
 
20
20
  Stack as of June 2026: `resend` 6.12.4, `@sendgrid/mail` 8.1.6, Postmark via its
21
21
  HTTP API, React Email 5.0 (React 19.2 / Next.js 16, Tailwind 4), Node 20+ / TS.
22
22
 
23
- ## The four non-negotiables
24
-
25
- Every email integration you produce must satisfy these. `scripts/verify.sh`
26
- greps for the first three; the fourth is the design constraint behind them.
27
-
28
- 1. **API key from env, never a literal.** `process.env.RESEND_API_KEY`, not
29
- `re_xxx` / `SG.xxx` / a server token in source. *Why:* a committed key is a
30
- send-as-you credential — instant abuse and reputation burn.
31
- 2. **Idempotency key on every transactional send.** *Why:* queues retry,
32
- serverless functions re-fire, users double-click. Without a stable key one
33
- password reset becomes three.
34
- 3. **Send on the correct stream.** Transactional and broadcast traffic go on
35
- distinct From + subdomain + stream. *Why:* a marketing reputation hit must
36
- never push password resets to spam.
37
- 4. **Verify the inbound webhook signature against the raw body.** *Why:* the
38
- bounce/complaint hook mutates your suppression list; an unverified hook lets
39
- anyone suppress (or un-suppress) your users.
23
+ `scripts/verify.sh` is read-only and greps a target for the four invariants this
24
+ skill exists to hold: env-sourced key, idempotency, a single `sendEmail()` seam,
25
+ and a webhook signature checked on the raw body.
40
26
 
41
27
  ## Step 1 — pick a provider
42
28
 
@@ -47,14 +33,18 @@ greps for the first three; the fourth is the design constraint behind them.
47
33
  | **Postmark** | Pure transactional, deliverability-first | No — self-dedupe via your key + webhooks | Postmark server templates | per-stream | Receipts/resets must never queue behind marketing |
48
34
 
49
35
  Idempotency support changes your strategy, not just your config — see Step 4.
50
- Full per-provider matrix (auth headers, exact signatures, webhook event names,
51
- rate limits) is in `references/providers.md`.
36
+ Full per-provider matrix (auth header, SDK + version, single/batch signatures,
37
+ idempotency model, stream/subdomain model, dynamic-template syntax, webhook event
38
+ names, rate limits, when to pick each) plus a suppression-webhook handler skeleton
39
+ per provider is in `references/providers.md`.
52
40
 
53
41
  ## Step 2 — the `sendEmail()` seam
54
42
 
55
43
  One provider-agnostic function. The rest of the app calls `sendEmail(...)` and
56
44
  never imports a provider SDK. *Why:* swapping SendGrid→Postmark is then one file,
57
- not a grep across every call site.
45
+ not a grep across every call site. That one file reads the key from
46
+ `process.env` — never a `re_…` / `SG.…` / server-token literal, because a
47
+ committed key is a send-as-you credential and burns your reputation with it.
58
48
 
59
49
  ```ts
60
50
  // lib/email/index.ts — the only place a provider SDK is imported
@@ -174,7 +164,9 @@ const html = '<h1>Hi ' + req.body.name + '</h1>'; // XSS + broken layout risk
174
164
 
175
165
  ## Step 4 — idempotency & retries
176
166
 
177
- Derive a deterministic key from the *event*, not the clock. Same event → same
167
+ Every transactional send carries a key, because queues retry, serverless
168
+ functions re-fire, and users double-click — without a stable key one password
169
+ reset becomes three. Derive it from the *event*, not the clock. Same event → same
178
170
  key → provider (or your table) collapses the duplicate.
179
171
 
180
172
  ```ts
@@ -245,7 +237,9 @@ down with it. The DNS/auth setup for those subdomains is
245
237
 
246
238
  The provider POSTs bounce and complaint events. Verify the signature on the
247
239
  **raw** body (parse after verifying), then write the address to a suppression
248
- list and check that list before every future send.
240
+ list and check that list before every future send. The verification is absolute
241
+ because this hook mutates the suppression list: unverified, anyone can suppress
242
+ — or un-suppress — your users.
249
243
 
250
244
  ```ts
251
245
  // app/api/email/webhook/route.ts (Next.js 16) — verify BEFORE parsing
@@ -281,14 +275,3 @@ before I ever send) is `../lead-gen/SKILL.md` / `../email-deliverability/SKILL.m
281
275
  | Trusting the webhook without signature check | Anyone can poison your suppression list | Verify signature on raw body, then parse |
282
276
  | Sending to a bounced/complained address | Reputation damage, ISP penalties | Filter against suppression list before send |
283
277
  | Calling the provider SDK at scattered call sites | Provider swap = grep across the app | One `sendEmail()` seam (Step 2) |
284
-
285
- ## Reference
286
-
287
- `references/providers.md` — full side-by-side matrix (auth header, SDK +
288
- version, single/batch signatures, idempotency model, stream/subdomain model,
289
- dynamic-template syntax, webhook event names, rate limits, when to pick each)
290
- plus a suppression-webhook handler skeleton per provider.
291
-
292
- Generic typed HTTP-client wrappers for *any* REST API →
293
- `../api-connector-builder/SKILL.md`. The orchestration that decides *when* to
294
- fire a multi-step sequence → `../automation-flows/SKILL.md`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: error-handling
3
- description: "Use when designing how code recognizes, recovers from, and reports failure — typed error taxonomies, retry/backoff/timeout policy, circuit breakers, React/Next error boundaries, and the user message + operator log line. Triggers: 'add retries with backoff', 'set up an error boundary', 'standardize our error types', 'stop swallowing exceptions', 'this catch block hides the real error', 'degrade gracefully when the payment provider is down', 'why does our retry storm take the database down', 'maneja los errores sin tragarte la excepción', 'afegeix reintents amb backoff i tallacircuits'. NOT finding why one specific thing broke (that is debug), NOT wiring logs/metrics/traces (that is observability), NOT the on-the-wire error envelope contract (that is api-design)."
3
+ description: "Use when designing the reaction to a class of failures — typed error taxonomies, retry/backoff/timeout policy, circuit breakers, React/Next error boundaries, and the user-message vs operator-log split. NOT diagnosing one specific crash (that is debug), NOT logs/metrics/traces (that is observability), NOT the wire error envelope (that is api-design)."
4
4
  tags: [error-handling, resilience, retry, circuit-breaker, error-boundary, typed-errors, fault-tolerance]
5
5
  recommends: [debug, observability, monitoring, api-design, secure-coding, testing-web, nextjs]
6
6
  origin: risco
@@ -9,23 +9,11 @@ origin: risco
9
9
  # Error handling — classify, contain, surface
10
10
 
11
11
  You are designing what happens *whenever anything in a class breaks*, not chasing
12
- one crash (that is [`debug`](../debug/SKILL.md)). The deliverable is a typed error
13
- taxonomy, a retry policy with caps and jitter, boundary placement, and a
12
+ one crash (that is [`debug`](../debug/SKILL.md)). Every failure gets classified,
13
+ contained, and surfaced — never swallowed. The deliverable, in that order: a typed
14
+ error taxonomy, a retry policy with caps and jitter, boundary placement, and a
14
15
  two-audience message contract — never a pile of `try { … } catch {}`.
15
16
 
16
- ## The one rule
17
-
18
- **Every failure gets classified, contained, and surfaced — never swallowed.**
19
-
20
- - **Classify** — give the failure a type so the right code can react to the right
21
- kind. An untyped `catch (e)` cannot decide anything.
22
- - **Contain** — stop one failing dependency from taking the process (or the DB)
23
- with it. No timeout, no cap = unbounded blast radius.
24
- - **Surface** — emit two things: a human message that never leaks internals, and
25
- an operator log with the full cause chain.
26
-
27
- The five steps below are those three jobs in order. Do them in order.
28
-
29
17
  ## Step 1 — Model failure as a taxonomy
30
18
 
31
19
  Bucket every failure into one of three kinds. The bucket dictates the reaction;
@@ -257,19 +245,6 @@ GOOD → 503 { "code": "upstream_unavailable", "correlationId": "a1b2c3" }
257
245
  More before/after rewrites and the copy contract are in
258
246
  [`references/boundaries-and-messaging.md`](references/boundaries-and-messaging.md).
259
247
 
260
- ## Decision table
261
-
262
- | Situation | Reaction |
263
- |---|---|
264
- | Expected domain outcome the caller must handle | `Result<T,E>` |
265
- | Programmer bug / invariant violation | `throw` → nearest boundary |
266
- | Transient failure on an idempotent call | retry (capped + full jitter) |
267
- | Transient failure on a mutation without a key | do **not** retry; require idempotency key first |
268
- | Dependency failing repeatedly | circuit-break + fallback |
269
- | Failure during React render | `error.tsx` boundary |
270
- | Failure in an event handler / async callback | explicit `try/catch` + state |
271
- | Unhandled at the top of the process | process handler → log + exit |
272
-
273
248
  ## Anti-patterns
274
249
 
275
250
  | Anti-pattern | Why it bites | Fix |
@@ -289,19 +264,10 @@ More before/after rewrites and the copy contract are in
289
264
  | One giant `try` around 200 lines | you cannot tell which call failed | scope `try` to the fallible call |
290
265
  | Treat every error as a retryable transient | masks real bugs as "flaky" | classify first (Step 1), then react |
291
266
 
292
- ## Verification checklist
293
-
294
- Before claiming the failure path is handled:
295
-
296
- - [ ] Every failure maps to one of the three buckets (Step 1).
297
- - [ ] No empty `catch {}` / `except: pass` / bare `except:`.
298
- - [ ] Retries are capped, jittered, transient-only, and idempotent-only.
299
- - [ ] Every outbound call has a per-attempt timeout.
300
- - [ ] Repeatedly-failing dependencies have a circuit breaker + fallback.
301
- - [ ] `error.tsx` / `global-error.tsx` placed correctly; render-only limits known.
302
- - [ ] User message has no internals + carries a correlation id; the log has the cause chain.
303
- - [ ] Cause is preserved on every wrap (no lost root error).
267
+ ## The gate
304
268
 
305
- Run `scripts/verify.sh <path>` to scan the working tree for the highest-signal
306
- anti-patterns above. It is **advisory** (exit 0 with warnings); pass `--strict` to
307
- make any hit fail. It is heuristic — it flags, you judge.
269
+ Before claiming the failure path is handled, read your diff against the table
270
+ above — those rows *are* the checklist. `scripts/verify.sh <path>` scans the
271
+ working tree for the highest-signal ones. It is **advisory** (exit 0 with
272
+ warnings); pass `--strict` to make any hit fail. It is heuristic — it flags,
273
+ you judge.
File without changes