sanook-cli 0.4.0

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.
Files changed (148) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +38 -0
  3. package/LICENSE +201 -0
  4. package/README.md +239 -0
  5. package/dist/agentContext.js +2 -0
  6. package/dist/approval.js +78 -0
  7. package/dist/bin.js +461 -0
  8. package/dist/brain.js +186 -0
  9. package/dist/commands.js +66 -0
  10. package/dist/compaction.js +85 -0
  11. package/dist/config.js +101 -0
  12. package/dist/cost.js +59 -0
  13. package/dist/diff.js +36 -0
  14. package/dist/gateway/auth.js +32 -0
  15. package/dist/gateway/ledger.js +94 -0
  16. package/dist/gateway/lock.js +114 -0
  17. package/dist/gateway/schedule.js +74 -0
  18. package/dist/gateway/scheduler.js +87 -0
  19. package/dist/gateway/serve.js +57 -0
  20. package/dist/gateway/server.js +94 -0
  21. package/dist/gateway/telegram.js +115 -0
  22. package/dist/git.js +55 -0
  23. package/dist/hooks.js +104 -0
  24. package/dist/knowledge.js +68 -0
  25. package/dist/loop.js +169 -0
  26. package/dist/mcp.js +191 -0
  27. package/dist/memory.js +108 -0
  28. package/dist/providers/codex.js +86 -0
  29. package/dist/providers/keys.js +37 -0
  30. package/dist/providers/models.js +55 -0
  31. package/dist/providers/registry.js +241 -0
  32. package/dist/session.js +36 -0
  33. package/dist/skill-install.js +190 -0
  34. package/dist/skills.js +111 -0
  35. package/dist/tools/bash.js +26 -0
  36. package/dist/tools/edit.js +107 -0
  37. package/dist/tools/git.js +68 -0
  38. package/dist/tools/index.js +36 -0
  39. package/dist/tools/list.js +24 -0
  40. package/dist/tools/permission.js +30 -0
  41. package/dist/tools/read.js +18 -0
  42. package/dist/tools/recall.js +12 -0
  43. package/dist/tools/remember.js +14 -0
  44. package/dist/tools/schedule.js +61 -0
  45. package/dist/tools/search.js +54 -0
  46. package/dist/tools/skill.js +65 -0
  47. package/dist/tools/task.js +46 -0
  48. package/dist/tools/util.js +5 -0
  49. package/dist/tools/write.js +27 -0
  50. package/dist/ui/app.js +132 -0
  51. package/dist/ui/banner.js +20 -0
  52. package/dist/ui/brain-wizard.js +29 -0
  53. package/dist/ui/render.js +57 -0
  54. package/dist/ui/setup.js +46 -0
  55. package/package.json +77 -0
  56. package/second-brain/AGENTS.md +18 -0
  57. package/second-brain/CLAUDE.md +96 -0
  58. package/second-brain/Evals/retrieval-eval.md +30 -0
  59. package/second-brain/GEMINI.md +15 -0
  60. package/second-brain/Home.md +33 -0
  61. package/second-brain/README.md +29 -0
  62. package/second-brain/Runbooks/ingest-quarantine.md +27 -0
  63. package/second-brain/Runbooks/sleep-time-consolidation.md +26 -0
  64. package/second-brain/Shared/AI-Context-Index.md +52 -0
  65. package/second-brain/Shared/Core-Facts/protected-facts.md +21 -0
  66. package/second-brain/Shared/Decision-Memory/decision-log.md +24 -0
  67. package/second-brain/Shared/Memory-Inbox/memory-inbox.md +23 -0
  68. package/second-brain/Shared/Operating-State/current-state.md +30 -0
  69. package/second-brain/Shared/Provenance/ingest-log.md +27 -0
  70. package/second-brain/Shared/Rules/context-assembly-policy.md +28 -0
  71. package/second-brain/Shared/Rules/frontmatter-standard.md +33 -0
  72. package/second-brain/Shared/Rules/skills-admission.md +30 -0
  73. package/second-brain/Shared/User-Memory/user-preferences.md +25 -0
  74. package/second-brain/Templates/bug.md +22 -0
  75. package/second-brain/Templates/handoff.md +21 -0
  76. package/second-brain/Templates/project.md +24 -0
  77. package/second-brain/Templates/session.md +26 -0
  78. package/second-brain/USER.md +36 -0
  79. package/second-brain/Vault Structure Map.md +106 -0
  80. package/skills/agent-tool-mcp-builder/SKILL.md +88 -0
  81. package/skills/api-design-review/SKILL.md +70 -0
  82. package/skills/async-concurrency-correctness/SKILL.md +93 -0
  83. package/skills/audit-accessibility-wcag/SKILL.md +59 -0
  84. package/skills/audit-technical-seo/SKILL.md +62 -0
  85. package/skills/auth-jwt-session/SKILL.md +88 -0
  86. package/skills/brainstorm-design/SKILL.md +73 -0
  87. package/skills/build-etl-pipeline/SKILL.md +58 -0
  88. package/skills/build-form-validation/SKILL.md +103 -0
  89. package/skills/build-office-docs/SKILL.md +80 -0
  90. package/skills/build-react-component/SKILL.md +116 -0
  91. package/skills/build-spreadsheet/SKILL.md +106 -0
  92. package/skills/caching-strategy/SKILL.md +75 -0
  93. package/skills/cicd-pipeline-author/SKILL.md +65 -0
  94. package/skills/cloud-cost-optimize/SKILL.md +91 -0
  95. package/skills/code-comments/SKILL.md +52 -0
  96. package/skills/code-review/SKILL.md +61 -0
  97. package/skills/db-migration-safety/SKILL.md +67 -0
  98. package/skills/debug-frontend-browser/SKILL.md +58 -0
  99. package/skills/debug-root-cause/SKILL.md +54 -0
  100. package/skills/dependency-upgrade/SKILL.md +56 -0
  101. package/skills/deploy-release/SKILL.md +64 -0
  102. package/skills/diff-table-parity/SKILL.md +58 -0
  103. package/skills/dockerfile-optimize/SKILL.md +82 -0
  104. package/skills/error-message/SKILL.md +58 -0
  105. package/skills/estimate-work/SKILL.md +54 -0
  106. package/skills/explore-codebase/SKILL.md +73 -0
  107. package/skills/git-commit-pr/SKILL.md +65 -0
  108. package/skills/gitops-deploy-workflow/SKILL.md +97 -0
  109. package/skills/implement-from-design/SKILL.md +69 -0
  110. package/skills/incident-response-sre/SKILL.md +78 -0
  111. package/skills/k8s-debug-workload/SKILL.md +135 -0
  112. package/skills/k8s-manifest-review/SKILL.md +86 -0
  113. package/skills/llm-eval-harness/SKILL.md +63 -0
  114. package/skills/manage-client-server-state/SKILL.md +94 -0
  115. package/skills/mermaid-diagram/SKILL.md +61 -0
  116. package/skills/message-queue-jobs/SKILL.md +139 -0
  117. package/skills/naming-helper/SKILL.md +57 -0
  118. package/skills/observability-instrument/SKILL.md +113 -0
  119. package/skills/optimize-core-web-vitals/SKILL.md +75 -0
  120. package/skills/optimize-sql-query/SKILL.md +67 -0
  121. package/skills/performance-profiling/SKILL.md +65 -0
  122. package/skills/process-pdf/SKILL.md +107 -0
  123. package/skills/profile-dataset/SKILL.md +97 -0
  124. package/skills/prompt-engineering/SKILL.md +70 -0
  125. package/skills/rag-pipeline/SKILL.md +53 -0
  126. package/skills/rate-limiting/SKILL.md +96 -0
  127. package/skills/refactor-cleanup/SKILL.md +54 -0
  128. package/skills/regex-build/SKILL.md +72 -0
  129. package/skills/release-notes/SKILL.md +79 -0
  130. package/skills/rest-graphql-contract/SKILL.md +71 -0
  131. package/skills/scrape-structured-web-data/SKILL.md +61 -0
  132. package/skills/secrets-management/SKILL.md +96 -0
  133. package/skills/security-review/SKILL.md +62 -0
  134. package/skills/shell-script-robust/SKILL.md +71 -0
  135. package/skills/style-responsive-tailwind/SKILL.md +70 -0
  136. package/skills/terraform-plan-review/SKILL.md +95 -0
  137. package/skills/type-safety-strict/SKILL.md +82 -0
  138. package/skills/validate-data-quality/SKILL.md +62 -0
  139. package/skills/wrangle-tabular-data/SKILL.md +75 -0
  140. package/skills/write-adr/SKILL.md +75 -0
  141. package/skills/write-analytical-sql/SKILL.md +71 -0
  142. package/skills/write-data-viz/SKILL.md +58 -0
  143. package/skills/write-docs/SKILL.md +54 -0
  144. package/skills/write-plan/SKILL.md +59 -0
  145. package/skills/write-playwright-e2e/SKILL.md +86 -0
  146. package/skills/write-prd/SKILL.md +65 -0
  147. package/skills/write-rfc/SKILL.md +75 -0
  148. package/skills/write-tests/SKILL.md +50 -0
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: build-etl-pipeline
3
+ description: Designs and implements ETL/ELT pipelines — extract from sources, transform/normalize, load to a warehouse — with idempotency, incremental loads, scheduling, and orchestration patterns.
4
+ when_to_use: When the user wants to build or refactor a data pipeline that moves data between systems — ingest from API/DB/files, transform and load into a warehouse, set up incremental/CDC loads, or structure an Airflow/dbt-style orchestration. Distinct from one-off tabular wrangling: use this for scheduled, multi-source, repeatable pipelines.
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Use when the work is a **repeatable, scheduled flow** that moves data from one or more sources into a sink/warehouse — not a one-off transform of a single file. Signals: "ingest from the API every hour", "incremental load", "CDC", "backfill", "dedup on load", "Airflow DAG", "dbt models", "the nightly job".
10
+
11
+ If it's a single ad-hoc clean/reshape of one dataset with no schedule, that's tabular wrangling, not this skill.
12
+
13
+ ## Steps
14
+
15
+ 1. **Pin the contract before writing code.** Lock down per source: format (JSON/CSV/Parquet/DB rows), grain (one row = ?), natural/business key, and the timestamp you'll use for incrementality (`updated_at`? event time? ingestion time?). Lock down per sink: target table, primary/unique key, partition column, and SLA (freshness target + max runtime). Write these as a short table — every later decision hangs off it.
16
+
17
+ 2. **Choose extraction mode per source — don't default to full reload.**
18
+ - **Full snapshot**: small/dimension tables, no reliable change marker. Reload whole table each run.
19
+ - **Incremental (watermark)**: source has a monotonic `updated_at`/`id`. Store last successful high-watermark in a state table/file; next run pulls `WHERE updated_at > :watermark`. Advance the watermark **only after the load commits**, never at extract time.
20
+ - **CDC**: high-volume mutable tables where you need deletes/updates. Consume a log (Debezium/WAL/binlog) or a change-tracking table; map `op = c/u/d` to upsert/delete.
21
+ - Always pull with **overlap** (`>=` watermark minus a small lag window, e.g. 5–15 min) to catch rows committed out of clock order, then rely on idempotent load to dedup.
22
+
23
+ 3. **Make extract resumable and bounded.** Paginate/chunk by key range or time window, not `OFFSET` (offset drifts under concurrent writes). Cap page size. Persist a per-window cursor so a crash resumes mid-source instead of restarting from zero. Land raw extracts to a staging/bronze layer first (immutable, partitioned by load date) before any transform — this is your replay buffer.
24
+
25
+ 4. **Transform in a staging layer with explicit schema mapping.** Normalize types and timezones (store UTC). Apply: dedup (keep latest by key + version/`updated_at`), surrogate keys (hash of natural key — stable across reloads, e.g. `md5(coalesce(cols))`), and schema mapping source→target column by column (no `SELECT *` into the warehouse). Handle **late-arriving data**: dimensions arriving after facts → either staging buffer + retry, or an "unknown" placeholder key you backfill later. Keep transforms deterministic so the same input always yields the same output (required for idempotency).
26
+
27
+ 5. **Load idempotently — every run must be safe to re-run.**
28
+ - Prefer `MERGE`/upsert keyed on the business/surrogate key, or **partition-overwrite** (delete-then-insert the affected partition in one transaction). Never bare `INSERT` from an at-least-once source — it duplicates on retry.
29
+ - Wrap delete+insert per partition in a transaction so a failure leaves the partition fully old, never half-loaded.
30
+ - For append-only event sinks, dedup by a unique event id (`INSERT ... ON CONFLICT DO NOTHING`).
31
+ - **Backfill = the same load path with a date-range param**, not a separate script. Backfill one partition/day at a time so reruns and forward jobs share idempotency guarantees.
32
+
33
+ 6. **Orchestrate with idempotent tasks and explicit deps.** Model as a DAG: `extract → stage → transform → load → validate`. Each task must be retry-safe in isolation. Set bounded retries with exponential backoff + jitter. Make tasks **parameterized by execution window** (the run's logical date), so a backfill of an old date hits exactly that partition. In Airflow, key off `execution_date`/`data_interval`, not `now()`. In dbt, use incremental models with a unique_key and `is_incremental()` filter. Set `max_active_runs` per DAG to avoid two runs racing the same watermark.
34
+
35
+ 7. **Isolate failures and observe.** Route bad rows to a **dead-letter / quarantine** table (with raw payload + error reason) instead of failing the whole batch — but fail loudly if the dead-letter rate crosses a threshold. Emit per-run metrics: rows in/out, rejected count, watermark advanced from→to, runtime. Add freshness + row-count + null-key assertions as a post-load `validate` task that fails the run (e.g. dbt tests, Great Expectations, or plain `SELECT` checks).
36
+
37
+ ## Common Errors
38
+
39
+ - **Watermark advanced before load committed** → on crash you skip rows permanently. Advance watermark only in the success path, after the load transaction commits.
40
+ - **Non-idempotent reload (bare INSERT) on an at-least-once source** → duplicates on every retry. Use MERGE/upsert or partition-overwrite; key on business/surrogate key.
41
+ - **Half-loaded partition** → reader sees old+new mixed, sums wrong. Delete+insert the partition inside one transaction; don't insert then separately delete.
42
+ - **`OFFSET`-based pagination under concurrent writes** → rows skipped or doubled as the table shifts. Paginate by key/time range with a stored cursor.
43
+ - **No overlap window on incremental pull** → rows committed slightly out of order (clock skew, long transactions) are missed forever. Re-pull a small lag window each run; idempotent load absorbs the overlap.
44
+ - **Schema drift breaks the load silently** → new/renamed/dropped source column. Validate the incoming schema against expected and fail (or quarantine) on mismatch; never `SELECT *` into the warehouse.
45
+ - **Orchestrator uses `now()` instead of the run's logical date** → backfills land in the wrong partition and reruns aren't reproducible. Parameterize every task by execution window.
46
+ - **Late-arriving dimensions create orphan facts** → fact rows point to a key the dim doesn't have yet. Use placeholder/unknown keys + a backfill pass, or buffer-and-retry.
47
+ - **One bad row kills the whole batch** → no progress until manual fix. Dead-letter the row, continue, alert on rate.
48
+
49
+ ## Verify
50
+
51
+ Run these checks before declaring done — show output, don't assert "it works":
52
+
53
+ 1. **Idempotency**: run the same window twice; row counts and key aggregates are identical the second time (no growth, no dups). `SELECT key, count(*) FROM target GROUP BY key HAVING count(*) > 1` returns zero rows.
54
+ 2. **Incrementality**: confirm watermark/state advances only on success — kill the job mid-load, rerun, verify no rows lost and none duplicated.
55
+ 3. **Backfill = forward path**: backfill one historical partition and confirm it produces the same shape as a live run for that date.
56
+ 4. **Failure isolation**: inject a malformed row; verify it lands in dead-letter and the run still completes (or fails only if over threshold).
57
+ 5. **Validation gate**: confirm the post-load assertions (freshness, row count, null-key) actually fail the run when violated — break one deliberately and watch it red.
58
+ 6. **Schema drift**: add/rename a source column in a fixture; confirm the pipeline fails or quarantines instead of loading garbage.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: build-form-validation
3
+ description: Implements type-safe forms with React Hook Form + Zod (or server-action validation) — schemas, field arrays, multi-step flows, and accessible error handling; used when building or fixing forms.
4
+ when_to_use: When the user builds or fixes a form, mentions React Hook Form, Zod, form validation, multi-step/wizard forms, field arrays, or server-action input validation.
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Use this skill when building or fixing a form in a React/Next.js codebase — anything touching `react-hook-form`, `zod`, `zodResolver`, field arrays, multi-step/wizard flows, or server-action input validation. This covers the **client form layer**: schema-driven validation, RHF wiring, accessible errors, and the client↔server-action contract.
10
+
11
+ Not this skill: pure REST/RPC contract or response-shape review (that is API design review). This skill stops at "the action received validated input and returned a typed result."
12
+
13
+ First, detect the stack before writing code:
14
+ - `grep -r "next" package.json` and check for an `app/` dir → App Router (server actions + `useActionState` available on React 19).
15
+ - Check installed versions: `react-hook-form` v7+, `zod` v3 vs v4 (error API differs), `@hookform/resolvers`.
16
+ - Look for an existing form in the repo and **match its pattern** (UI lib, resolver setup, error component) instead of inventing a new one.
17
+
18
+ ## Steps
19
+
20
+ 1. **One Zod schema = source of truth.** Write the schema once, derive the TS type with `z.infer`, and import the *same* schema on client and server. Never hand-write a parallel `interface`.
21
+ ```ts
22
+ // schema.ts — shared
23
+ export const signupSchema = z.object({
24
+ email: z.string().email(),
25
+ password: z.string().min(8),
26
+ confirm: z.string(),
27
+ }).refine((d) => d.password === d.confirm, {
28
+ message: "Passwords must match",
29
+ path: ["confirm"], // attaches error to the field, not the form root
30
+ });
31
+ export type SignupInput = z.infer<typeof signupSchema>;
32
+ ```
33
+ For inputs that arrive as strings but mean numbers/dates, use `z.coerce.number()` / `z.coerce.date()` so form values and server `FormData` both parse.
34
+
35
+ 2. **Wire React Hook Form with the resolver.** Use `zodResolver`, set `defaultValues` for *every* field (uncontrolled inputs need them or they switch controlled→uncontrolled mid-edit), and pick a sane `mode`.
36
+ ```ts
37
+ const form = useForm<SignupInput>({
38
+ resolver: zodResolver(signupSchema),
39
+ defaultValues: { email: "", password: "", confirm: "" },
40
+ mode: "onTouched", // validate after blur, then re-validate onChange — best UX default
41
+ });
42
+ ```
43
+ - Native inputs (`<input>`, `<select>`): use `{...form.register("email")}`.
44
+ - Custom/headless components (Select, DatePicker, anything not exposing a ref): wrap in `<Controller name=... control={form.control} render={...} />`. Mixing `register` on a controlled component silently drops values.
45
+
46
+ 3. **Field arrays + nested objects** use `useFieldArray`. Key the rows by `field.id` (RHF's stable id), **never by array index** — index keys corrupt state on remove/reorder.
47
+ ```ts
48
+ const { fields, append, remove } = useFieldArray({ control: form.control, name: "items" });
49
+ // register nested path:
50
+ {...form.register(`items.${index}.name`)}
51
+ ```
52
+ Read nested errors via `form.formState.errors.items?.[index]?.name`.
53
+
54
+ 4. **Multi-step / wizard:** one sub-schema per step; validate only the current step's fields before advancing with `await form.trigger(["fieldA","fieldB"])`. Keep all steps in **one** `useForm` instance (do not remount per step or you lose state). Persist between steps with a single `useForm` + (optionally) `localStorage`/URL state; validate the *full* schema on final submit. Compose with `signupSchema.pick({...})` or `.partial()` per step so the final type stays one source of truth.
55
+
56
+ 5. **Next.js server actions** — validate inside the action, **return** typed errors, do not `throw` for validation:
57
+ ```ts
58
+ "use server";
59
+ export async function signup(_prev: State, formData: FormData): Promise<State> {
60
+ const parsed = signupSchema.safeParse(Object.fromEntries(formData));
61
+ if (!parsed.success) {
62
+ return { ok: false, errors: parsed.error.flatten().fieldErrors };
63
+ }
64
+ // ...mutate DB...
65
+ revalidatePath("/dashboard"); // or revalidateTag — refresh cache after mutation
66
+ return { ok: true };
67
+ }
68
+ ```
69
+ On the client (React 19): `const [state, action, pending] = useActionState(signup, { ok: false });`. Bind RHF to the action with `<form action={action}>` and `form.handleSubmit` for client-side pre-check, or surface `state.errors` back into RHF via `form.setError`. **Always** re-validate server-side — client validation is UX, not security.
70
+
71
+ 6. **Accessible errors** on every field:
72
+ - `aria-invalid={!!errors.email}` on the input.
73
+ - `aria-describedby="email-error"` pointing at the error node; the error node has `id="email-error"` and `role="alert"` (or sits in an `aria-live="polite"` region) so SRs announce it.
74
+ - Associate a real `<label htmlFor>`; placeholders are not labels.
75
+ - On failed submit, **focus the first errored field**. RHF does this with `shouldFocusError: true` (default) for registered native inputs; for `Controller`/custom inputs, focus manually in the submit-error handler.
76
+
77
+ 7. **Async validation** (e.g. "username taken"): debounce the check (~300–500ms), and validate server-side too. Either a Zod `.refine(async ...)` (requires `parseAsync`/`safeParseAsync` — `zodResolver` handles this) or `form.setError("username", { message })` after a fetch. Disable submit while `formState.isValidating || isSubmitting` to prevent races.
78
+
79
+ ## Common Errors
80
+
81
+ - **`defaultValues` missing → "controlled to uncontrolled" warning** and lost values. Always seed every field, including arrays (`{ items: [] }`).
82
+ - **Field array keyed by index** → on `remove(2)` the wrong rows re-render / values shift. Key by `field.id`.
83
+ - **`Controller` value not updating** because the inner component fires a non-standard onChange — map it: `onChange={(v) => field.onChange(v)}`. Don't spread `register` onto a controlled component.
84
+ - **`refine`/`superRefine` error lands on form root, not the field** → user sees no inline message. Set `path: ["confirm"]`.
85
+ - **`z.coerce` forgotten for number/checkbox inputs** → `FormData` gives `"3"`/`"on"`, schema expects `number`/`boolean`, every submit fails validation silently.
86
+ - **Server action `throw` for invalid input** → blows up as an error boundary / 500 instead of inline errors. Use `safeParse` + return.
87
+ - **Forgot `revalidatePath`/`revalidateTag` after mutation** → UI shows stale cached data post-submit even though the DB changed.
88
+ - **Re-mounting `useForm` per wizard step** wipes earlier answers. One instance, conditionally render steps.
89
+ - **Trusting client validation only** — bypassable; the server action must re-`safeParse`.
90
+ - **Zod v3 vs v4 mismatch**: `.flatten()` and the error object shape changed across majors. Check the installed version before copying error-extraction code.
91
+ - **`mode: "onChange"` on a big form** → validates on every keystroke, janky + noisy errors before the user finishes typing. Prefer `onTouched` / `onBlur`.
92
+
93
+ ## Verify
94
+
95
+ Run these before declaring done — show evidence, not just "fixed":
96
+
97
+ 1. **Type check:** `npx tsc --noEmit` (or the project's typecheck script) passes — confirms `z.infer` and form generics line up.
98
+ 2. **Lint:** project ESLint passes on touched files.
99
+ 3. **Happy path:** submit valid data → action runs, success state shows, cache revalidates (data refreshes without a hard reload).
100
+ 4. **Validation path:** submit each invalid field → inline error appears under the right field, submit is blocked, no 500/error boundary.
101
+ 5. **A11y spot-check:** invalid field has `aria-invalid="true"` and `aria-describedby` resolving to a visible error node; first errored field receives focus on failed submit. Verify in the DOM/accessibility tree (e.g. devtools snapshot), not by eye alone.
102
+ 6. **Field array / wizard** (if present): add → remove a middle row → values of remaining rows stay correct; advancing a step only validates that step; final submit validates the whole schema.
103
+ 7. If a contract or behavior is testable, add/run a unit test on the schema (`safeParse` of good + bad payloads) so the validation can't silently regress.
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: build-office-docs
3
+ description: Generates and edits Office documents (DOCX/PPTX) programmatically from data or templates, including styled reports, tables, headers/footers, tracked changes, and slide decks.
4
+ when_to_use: When the user asks to create or edit a Word document or PowerPoint deck — generate a report, fill a DOCX/PPTX template, mail-merge data into letters/memos, build slides from an outline, or apply tracked-changes edits to an existing .docx.
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Trigger this skill when the task produces or modifies a `.docx` or `.pptx` file:
10
+
11
+ - Generate a Word report/letter/memo from data or a written outline.
12
+ - Fill a DOCX/PPTX **template** that has `{{placeholder}}` fields or named shapes.
13
+ - **Mail-merge** one template over a row set (one output file per row, or one file with repeated sections).
14
+ - Build a slide deck from a structured outline (title + bullets + notes per slide).
15
+ - Edit an **existing** `.docx`: find-replace, insert/restyle tables, add headers/footers/page numbers, or apply **tracked changes**.
16
+
17
+ Do NOT use this for: plain `.txt`/`.md`/`.csv` output, PDFs (use a PDF/HTML-to-PDF path), or `.doc`/`.ppt` legacy binary formats (convert to OOXML first via LibreOffice `soffice --headless --convert-to docx`).
18
+
19
+ ## Steps
20
+
21
+ 1. **Classify the job along two axes before writing code:**
22
+ - Format: `docx` (Word) vs `pptx` (PowerPoint). Decide from the request, not the extension alone.
23
+ - Mode: `from-scratch` (build a new file) · `template` (fill placeholders in an existing file) · `edit-existing` (mutate a real document, preserving its styles).
24
+ Print the chosen `{format, mode}` before proceeding — the library and approach differ per cell.
25
+
26
+ 2. **Pick the library by {format, mode}. Do not invent a new dependency if one is already in the repo:**
27
+ - docx, from-scratch / edit-existing → `python-docx`.
28
+ - docx, template with `{{fields}}`/Jinja loops → `docxtpl` (renders Jinja2 inside a real .docx, keeps styles).
29
+ - pptx, any mode → `python-pptx`.
30
+ - Low-level changes these libraries can't express (tracked changes `w:ins`/`w:del`, custom XML, content controls, theme colors) → edit the OOXML directly: a `.docx`/`.pptx` is a **zip** of XML parts (`word/document.xml`, `ppt/slides/slideN.xml`). Unzip, edit XML via `lxml`, re-zip.
31
+ Confirm the package is installed (`pip show python-docx docxtpl python-pptx`); install into the project env, not globally, if missing.
32
+
33
+ 3. **DOCX — from scratch (`python-docx`):**
34
+ - Structure with real styles, not manual formatting: `doc.add_heading(text, level=N)`, `doc.add_paragraph(text, style='List Bullet'|'List Number')`. Reuse `doc.styles` / a base template (`Document('template.docx')`) so output inherits the org's fonts and theme.
35
+ - Tables: `t = doc.add_table(rows, cols)`, set `t.style = 'Light Grid Accent 1'` (must be a style that exists in the doc, else it errors). Cell styling lives on the **run** inside the cell paragraph: `cell.paragraphs[0].add_run(text).bold = True`; widths need `tblLayout` fixed (`t.autofit = False` + set each `cell.width`).
36
+ - Headers/footers/page numbers: `section.header` / `section.footer`; a page-number field requires a `fldSimple`/`w:instrText PAGE` XML run (`python-docx` has no helper — inject the XML via `run._r.append(...)`).
37
+ - Images: `doc.add_picture(path, width=Inches(...))`; missing files raise, so check the path first.
38
+
39
+ 4. **DOCX — template fill & mail-merge (`docxtpl`):**
40
+ - Author the template with Jinja in real Word text: `{{ customer_name }}`, `{% for row in items %}...{% endfor %}` (use `{%tr ... %}` to repeat **table rows**, `{%p ... %}` to repeat paragraphs). Tag names must match the context dict keys exactly.
41
+ - Render: `tpl = DocxTemplate('tpl.docx'); tpl.render(context); tpl.save(out)`.
42
+ - Mail-merge = loop the data rows: render one output per row → `out_{i}.docx` (or `_{key}.docx`), OR pass a list into one template and use a `{%tr%}`/`{%p%}` loop for a single combined file. Decide which the user wants up front.
43
+ - Insert images/rich runs via `tpl.new_subdoc()` / `InlineImage`, not raw strings, so styling survives.
44
+
45
+ 5. **PPTX (`python-pptx`):**
46
+ - Start from `Presentation('template.pptx')` to inherit master/layouts; `Presentation()` alone gives the default theme only.
47
+ - Per slide: pick a layout (`prs.slide_layouts[idx]`), `slide = prs.slides.add_slide(layout)`, then fill **named placeholders** by index/type (`slide.placeholders[0].text = title`) — do not assume placeholder 1 is always the body; inspect `[ph.placeholder_format.idx for ph in slide.placeholders]`.
48
+ - Bullets: write into the body placeholder's `text_frame`, one `paragraph` per bullet, set `paragraph.level` for indent.
49
+ - Speaker notes: `slide.notes_slide.notes_text_frame.text = notes`.
50
+ - Charts/tables: `slide.shapes.add_chart(...)` with a `CategoryChartData`, or `add_table(rows, cols, x, y, w, h)`.
51
+
52
+ 6. **Edit-existing without nuking styles:** open the real file, mutate only target nodes. For find-replace in `python-docx`, replace at the **run** level (text can be split across runs — naive `paragraph.text = ...` drops formatting); join/split runs carefully or use a known replace helper.
53
+
54
+ 7. **Tracked changes** are not in `python-docx`'s API — do it at the XML layer: an insertion is a `<w:ins w:author w:date>` wrapping the new run; a deletion wraps the old run in `<w:del>` with the text in `<w:delText>`. Set author/date attributes. Verify Word shows them under Review → Track Changes after opening.
55
+
56
+ 8. **Render to the requested path, then validate (step into ## Verify) before declaring done.** Report the absolute output path and confirm the file opens.
57
+
58
+ ## Common Errors
59
+
60
+ - **Corrupt file after manual XML edits** — re-zipping with wrong compression/structure, missing `[Content_Types].xml`, or a stray byte breaks the package and Word says "needs repair". Always round-trip through `zipfile` (preserve all parts), and re-open with the library after writing to catch corruption early.
61
+ - **Style does not exist → exception or silent no-op.** `t.style = 'Some Style'` / `add_paragraph(style=...)` only works if that style is defined in the document. Build from a template that contains the style, or add the style definition first. Style names are case- and space-sensitive.
62
+ - **Fonts/styles "not embedded" → output looks wrong on another machine.** OOXML references fonts by name; it does not embed them. If a specific font is required, embed it (`<w:embedRegular>` font part) or restrict to fonts the consumer has. Theme colors only resolve if the theme part is present (another reason to start from a real template).
63
+ - **Merge-field / placeholder mismatch.** A `{{tag}}` with no matching context key renders empty or raises (`jinja2.UndefinedError`); a key with no tag is silently ignored. Diff the template's tag set against the context dict keys before rendering. Word sometimes splits `{{ tag }}` across runs so `docxtpl` can't see it — keep each tag typed in one run (retype it cleanly in Word).
64
+ - **find-replace drops formatting** because Word stores one logical word across multiple `<w:r>` runs. Replacing `paragraph.text` collapses runs and loses bold/italic/links. Operate run-by-run or use a run-aware replacer.
65
+ - **PPTX placeholder index assumption** — layouts differ; index 1 is not always the content body. Always enumerate `slide.placeholders` and match by `placeholder_format.type`/`idx`.
66
+ - **`add_picture`/template path is relative** and the working dir differs at run time → `FileNotFoundError`. Use absolute paths.
67
+ - **Tracked changes invisible** because `w:author`/`w:date` are missing or the run wasn't wrapped in `w:ins`/`w:del` — Word then shows the edit as a normal change. Confirm the wrapping nodes and attributes exist.
68
+
69
+ ## Verify
70
+
71
+ A generated/edited Office file is done only when ALL hold:
72
+
73
+ - [ ] The file exists at the reported absolute path and has non-trivial size (> a few KB; a 0-byte or tiny file means the write failed).
74
+ - [ ] **It is a valid OOXML package:** `unzip -l <file>` lists parts including `[Content_Types].xml` and `word/document.xml` (docx) or `ppt/presentation.xml` (pptx) — no zip error.
75
+ - [ ] **It re-opens with the library** without exception: `Document(out)` (docx) or `Presentation(out)` (pptx) loads, and a spot-check reads back expected content (a heading's text, a known cell value, slide count == expected).
76
+ - [ ] For templates/mail-merge: no `{{` / `}}` / `{%` literals remain in the output (`unzip -p <file> word/document.xml | grep -c '{{'` returns 0), and one output file exists per data row when per-row mode was chosen.
77
+ - [ ] For tracked changes: `word/document.xml` contains the expected `<w:ins>`/`<w:del>` nodes with author+date; opening in Word shows them under Review.
78
+ - [ ] If a specific font/style/theme was requested, it is present (style name resolves; theme/font part exists in the zip).
79
+
80
+ If validation cannot run (no Word/LibreOffice available and the library re-open is the only check), state that the file passed structural+library validation but was not opened in a real Office client.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: build-react-component
3
+ description: Scaffolds production-grade React/Next.js components with proper props typing, server vs client component boundaries, and composition; used when building or restructuring UI components.
4
+ when_to_use: When the user asks to create, scaffold, or restructure a React/Next.js component, page, or layout — especially deciding Server vs Client Component, prop typing, and folder structure.
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Use this skill when creating, scaffolding, or restructuring a React/Next.js component, page, or layout. It is most valuable when you must decide Server vs Client Component boundaries, design a typed props contract, or lay out files. Skip it for trivial edits to an existing component (text/className tweaks, prop renames) — just edit directly.
10
+
11
+ Assumes Next.js App Router + React 19 + TypeScript. For pages/ router or React ≤18, drop the App Router steps and the `useActionState`/server-action patterns.
12
+
13
+ ## Steps
14
+
15
+ 1. **Default to a Server Component. Add `'use client'` only when the component actually needs the browser.** A file is client-only if it uses any of: `useState`/`useReducer`, `useEffect`/`useLayoutEffect`, event handlers (`onClick`, `onChange`, …), refs to DOM nodes, browser APIs (`window`, `localStorage`, `IntersectionObserver`), or a third-party lib that calls these. None of those → leave it a Server Component (no directive). `'use client'` is a **boundary**, not a per-file tag: it marks the entry point; every child imported into it becomes client too. So push it **down the tree** to the smallest interactive leaf.
16
+
17
+ 2. **Keep the boundary thin: pass Server Components into Client Components as `children`/props, don't import them.** A Client Component can *render* Server Component output if it arrives via props/children — it just can't `import` one. Pattern:
18
+ ```tsx
19
+ // page.tsx (server) — fetches data, composes
20
+ <ClientShell><ServerHeavyList /></ClientShell>
21
+ ```
22
+ This keeps the data-fetching + heavy markup on the server while the interactive wrapper stays small.
23
+
24
+ 3. **Write the props contract first, as a named `interface`, with defaults.**
25
+ ```tsx
26
+ interface CardProps {
27
+ title: string;
28
+ description?: string;
29
+ variant?: 'default' | 'outline'; // union, not string
30
+ children?: React.ReactNode;
31
+ }
32
+ export function Card({ title, description, variant = 'default', children }: CardProps) { … }
33
+ ```
34
+ Rules: no `React.FC` (breaks generics, implies legacy `children`). Use string-literal unions over `string` for variants. Default values in the destructure, not `defaultProps`. Extend native props when wrapping an element: `interface ButtonProps extends React.ComponentProps<'button'> { variant?: … }` then spread `{...rest}` onto the element so consumers keep `aria-*`, `type`, `onClick`, etc.
35
+
36
+ 4. **Fetch data in the Server Component with `async`/`await` — no `useEffect` fetching.**
37
+ ```tsx
38
+ export default async function Page() {
39
+ const data = await getData(); // runs on server, no client JS shipped
40
+ return <View data={data} />;
41
+ }
42
+ ```
43
+ `fetch` is auto-deduped/cached per request. Set freshness with `{ next: { revalidate: 60 } }` or `cache: 'no-store'`. Never lift this into a Client Component — that ships the fetch + waterfalls it.
44
+
45
+ 5. **Colocate files; reach for App Router special files when the route needs them.**
46
+ ```
47
+ components/card/
48
+ card.tsx # component
49
+ card.test.tsx # test
50
+ card.module.css # styles (or Tailwind inline)
51
+ app/dashboard/
52
+ page.tsx # route UI
53
+ layout.tsx # shared shell (persists across child routes)
54
+ loading.tsx # Suspense fallback — auto-wraps page
55
+ error.tsx # 'use client' error boundary (gets error + reset)
56
+ not-found.tsx
57
+ ```
58
+ Add `loading.tsx`/`error.tsx` only when the route does async work or can fail. Barrel `index.ts` files only at package/public boundaries — not inside feature folders (they bloat bundles and create import cycles).
59
+
60
+ 6. **Compose, don't configure.** Prefer `children` and slot props over a pile of booleans. When a component has tightly-coupled parts, use the **compound pattern** instead of `tabs={[…]}` config:
61
+ ```tsx
62
+ <Tabs defaultValue="a">
63
+ <Tabs.List><Tabs.Trigger value="a">A</Tabs.Trigger></Tabs.List>
64
+ <Tabs.Panel value="a">…</Tabs.Panel>
65
+ </Tabs>
66
+ ```
67
+ Shared state goes through a Context created *inside* the parent (see step 8). This beats prop explosions and lets consumers control layout.
68
+
69
+ 7. **For forms/mutations use Server Actions + React 19 hooks, not manual fetch + useState.**
70
+ ```tsx
71
+ // actions.ts
72
+ 'use server';
73
+ export async function save(prev, formData: FormData) { …; return { ok: true }; }
74
+ ```
75
+ ```tsx
76
+ 'use client';
77
+ const [state, action, pending] = useActionState(save, null);
78
+ return <form action={action}><button disabled={pending}>Save</button></form>;
79
+ ```
80
+ Use `useOptimistic` to reflect the change in UI before the action resolves; use `useFormStatus()` inside a child to read pending state without prop-passing.
81
+
82
+ 8. **Kill prop drilling by lifting state or scoping a Context — not by threading props 3+ levels.** If two siblings need the same state, lift it to their nearest common parent. If many descendants need it, create a typed Context **co-located with the feature** (provider in the parent, a `useX()` hook that throws if used outside the provider):
83
+ ```tsx
84
+ const Ctx = React.createContext<T | null>(null);
85
+ export function useTabs() {
86
+ const c = React.useContext(Ctx);
87
+ if (!c) throw new Error('useTabs must be used within <Tabs>');
88
+ return c;
89
+ }
90
+ ```
91
+ Don't reach for global state (Zustand/Redux) for what is local UI coordination.
92
+
93
+ 9. **Apply the accessibility baseline, then hand deep a11y to the a11y skill.** Use the semantic element (`<button>` for actions, `<a>`/`<Link>` for navigation, `<nav>`/`<main>`/`<ul>`), associate every input with a `<label htmlFor>`, give icon-only controls an `aria-label`, and don't trap keyboard users. Anything beyond this baseline (focus management, ARIA widget roles, live regions) → defer to the dedicated accessibility skill.
94
+
95
+ ## Common Errors
96
+
97
+ - **`useState`/event handler in a Server Component** → build error: *"You're importing a component that needs useState… add 'use client'."* Fix: add the directive to that leaf, or move just the interactive bit into a small client child. Don't slap `'use client'` on the whole page.
98
+ - **Importing a Server Component into a Client Component** → it silently becomes a client component (loses server-only access to DB/secrets/`async`). Fix: pass it as `children`/prop from a server parent (step 2).
99
+ - **Passing a non-serializable prop (function, Date, class instance) across the server→client boundary** → *"Only plain objects can be passed to Client Components."* Event handlers/functions can't cross it. Fix: serialize, or move the handler into the client side.
100
+ - **`React.FC<Props>`** → swallows generics and implies an optional `children` you may not want. Use a plain function with a typed param.
101
+ - **Fetching in `useEffect` for first paint** → request waterfall + spinner + larger bundle. Fix: `await` it in the Server Component (step 4).
102
+ - **`async` Client Component** (`'use client'` + `async function`) → not supported; only Server Components can be async. Fetch on the server or use a data hook / `use()`.
103
+ - **Reading `process.env.SECRET` in client code** → it's `undefined` in the browser (only `NEXT_PUBLIC_*` is exposed) and leaks if it weren't. Keep secrets in Server Components/actions.
104
+ - **Barrel `index.ts` re-exporting a whole feature folder** → defeats tree-shaking, drags client code into server bundles, and risks circular imports. Import from the specific file.
105
+ - **`error.tsx` without `'use client'`** → error boundaries must be Client Components; it won't catch otherwise.
106
+ - **Mutating data then expecting the UI to refresh** → call `revalidatePath`/`revalidateTag` in the server action; client state won't update on its own.
107
+
108
+ ## Verify
109
+
110
+ - [ ] `npx tsc --noEmit` passes — props contract is sound, no `any` leaking in.
111
+ - [ ] `next build` (or `next dev` with no console errors) — confirms no Server/Client boundary violation or serialization error.
112
+ - [ ] Every `'use client'` sits on the **smallest** interactive component; server data-fetching stays server-side (grep the tree: a `'use client'` file should not contain `await fetch`/DB calls).
113
+ - [ ] Props use named unions + extend native element props where wrapping; no `React.FC`; no `defaultProps`.
114
+ - [ ] Interactive controls are real semantic elements with labels/`aria-label`; tab order works.
115
+ - [ ] No prop drilled past 2 levels without a lift or Context; no global store for purely local UI state.
116
+ - [ ] Tests/lint run green if the repo has them (`npm test`, `npm run lint`).
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: build-spreadsheet
3
+ description: Creates and edits XLSX workbooks with formulas, multi-sheet references, cell formatting, conditional formatting, pivot-style summaries, and native Excel charts.
4
+ when_to_use: When the user asks to build or modify an Excel/XLSX file — add columns with formulas (SUM/VLOOKUP), format cells, build multi-sheet workbooks with cross-sheet references, create native charts, or turn data into a structured financial-model-style workbook.
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Use this skill to **produce a `.xlsx` artifact a human will open in Excel** — a financial model, budget, tracker, report, or dashboard. The deliverable is a live workbook: formulas recalculate, charts re-render, sheets cross-reference.
10
+
11
+ Reach for it when the request includes any of: "add a column that sums/looks-up", "format these cells", "highlight values over X", "build a summary sheet that pulls from the detail tabs", "make a chart", "turn this CSV into a real Excel model".
12
+
13
+ **Not this skill** — if the goal is to *analyze* data and report numbers back in chat (use pandas/data-wrangle). The line: data-wrangle answers a question, build-spreadsheet hands over a file.
14
+
15
+ Default engine: **openpyxl** (read + write + formatting + native charts in one library). Only switch to `xlsxwriter` for write-only generation of very large files (>50k rows) where speed matters — it is faster but **cannot read or edit existing files**.
16
+
17
+ ## Steps
18
+
19
+ 1. **Install + import.** `pip install openpyxl` if missing. Then:
20
+ ```python
21
+ from openpyxl import Workbook, load_workbook
22
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
23
+ from openpyxl.formatting.rule import ColorScaleRule, CellIsRule, FormulaRule
24
+ from openpyxl.chart import BarChart, LineChart, PieChart, ScatterChart, Reference, Series
25
+ from openpyxl.utils import get_column_letter
26
+ ```
27
+ New file → `wb = Workbook()`. Editing an existing file → `wb = load_workbook("in.xlsx")` (add `data_only=False` to keep formulas as formulas, not last-cached values).
28
+
29
+ 2. **Lay out sheets and headers.** `ws = wb.active; ws.title = "Detail"`; add more with `wb.create_sheet("Summary")`. Write headers, then write **typed** cell values — pass real `int`/`float`/`datetime`, not strings, or Excel treats numbers as text and SUM returns 0. Set `cell.number_format = "yyyy-mm-dd"` for dates.
30
+
31
+ 3. **Write formulas as strings, never precompute.** Store the formula so Excel computes it:
32
+ ```python
33
+ ws["D2"] = "=B2*C2"
34
+ ws["E2"] = "=SUM(D2:D100)"
35
+ ws["F2"] = '=VLOOKUP(A2,Detail!$A$2:$C$100,3,FALSE)'
36
+ ```
37
+ A leading `=` is what makes it a formula. openpyxl does **not** evaluate it — the cell has no value until Excel/LibreOffice opens and recalcs the file.
38
+
39
+ 4. **Cross-sheet refs and named ranges.** Reference another sheet with `SheetName!A1`; quote sheet names containing spaces: `'Q1 Detail'!A1`. For reusable ranges:
40
+ ```python
41
+ from openpyxl.workbook.defined_name import DefinedName
42
+ wb.defined_names.add(DefinedName("tax_rate", attr_text="Assumptions!$B$1"))
43
+ ws["C2"] = "=B2*tax_rate"
44
+ ```
45
+
46
+ 5. **Formatting.** Apply per cell (styles do **not** cascade from columns/rows):
47
+ ```python
48
+ ws["A1"].font = Font(bold=True, color="FFFFFF")
49
+ ws["A1"].fill = PatternFill("solid", fgColor="305496")
50
+ ws["B2"].number_format = '#,##0.00' # or '$#,##0', '0.0%', '#,##0;[Red](#,##0)'
51
+ ws.column_dimensions["A"].width = 22
52
+ ws.freeze_panes = "A2" # lock header row
53
+ ```
54
+
55
+ 6. **Conditional formatting** is bound to a range and re-evaluates live in Excel:
56
+ ```python
57
+ ws.conditional_formatting.add("D2:D100",
58
+ ColorScaleRule(start_type="min", start_color="F8696B",
59
+ end_type="max", end_color="63BE7B"))
60
+ ws.conditional_formatting.add("E2:E100",
61
+ CellIsRule(operator="greaterThan", formula=["1000"],
62
+ fill=PatternFill("solid", fgColor="FFC7CE")))
63
+ ```
64
+
65
+ 7. **Pivot-style summary** — don't try to write a real PivotTable (openpyxl support is fragile). Instead build a summary sheet of unique keys + `SUMIF`/`COUNTIF`/`AVERAGEIF` against the detail sheet, so totals stay live:
66
+ ```python
67
+ ws_sum["B2"] = '=SUMIF(Detail!$A:$A,A2,Detail!$D:$D)'
68
+ ```
69
+
70
+ 8. **Native charts** bound to `Reference` ranges (these recalc/redraw in Excel, unlike pasted images):
71
+ ```python
72
+ chart = BarChart(); chart.title = "Sales by Region"; chart.type = "col"
73
+ data = Reference(ws, min_col=2, min_row=1, max_col=2, max_row=10) # include header row
74
+ cats = Reference(ws, min_col=1, min_row=2, max_row=10)
75
+ chart.add_data(data, titles_from_data=True)
76
+ chart.set_categories(cats)
77
+ ws.add_chart(chart, "H2") # anchor cell
78
+ ```
79
+ Swap `BarChart`→`LineChart`/`PieChart`/`ScatterChart` as needed. Scatter needs `Series(yvalues, xvalues)` explicitly.
80
+
81
+ 9. **Save** with `wb.save("out.xlsx")`. Pick a clear, descriptive filename.
82
+
83
+ ## Common Errors
84
+
85
+ - **Formula shows as text / SUM = 0.** Either the string lacks a leading `=`, or the summed cells hold strings not numbers. Write numeric values as real `int`/`float`. Also: a cell can hold a formula **or** a literal value, not both — openpyxl never fills in the computed result, so the file looks "empty" until opened and recalced.
86
+ - **Reading back gives `None` for formula cells.** `load_workbook(data_only=True)` returns the *last cached value Excel saved*. A file written purely by openpyxl was never opened by Excel, so there is no cache → `None`. Use `data_only=False` to read the formula string; open in Excel once and save if you need cached values.
87
+ - **Styles/charts vanish after editing an existing file.** `load_workbook` drops what it can't model — VBA macros (unless `keep_vba=True`), some chart types, and many embedded PivotTables/images are lost on re-save. Verify after round-trip; for macro files keep `.xlsm` + `keep_vba=True`.
88
+ - **Style applied to a whole column doesn't show.** openpyxl styles are per-cell; setting `column_dimensions[...].font` only affects *new* cells, not existing ones. Loop the actual cells.
89
+ - **A `Font`/`Fill`/`Border` object shared across cells** can raise `StyleProxy`/copy errors — create a fresh style object (or `copy()`) per cell instead of reusing one instance.
90
+ - **Sheet name with spaces in a formula not quoted** → `#REF!`. Wrap in single quotes: `'My Sheet'!A1`.
91
+ - **Huge files blow memory.** For tens of thousands of rows, write with `Workbook(write_only=True)` + `ws.append(row_list)` (no random cell access, no formatting mid-stream), or use `xlsxwriter`.
92
+ - **Color hex** is `RRGGBB` (or `AARRGGBB`), no `#`. `"#FF0000"` fails silently or errors.
93
+
94
+ ## Verify
95
+
96
+ After saving, reopen and assert structure programmatically — never assume the write succeeded:
97
+
98
+ ```python
99
+ chk = load_workbook("out.xlsx")
100
+ assert {"Detail", "Summary"} <= set(chk.sheetnames)
101
+ assert chk["Detail"]["D2"].value == "=B2*C2" # formula stored as string
102
+ assert chk["Detail"]["D2"].data_type == "f" # 'f' = formula
103
+ assert len(chk["Detail"]._charts) >= 1 # chart survived save
104
+ ```
105
+
106
+ Then do a real recalc check: open the file once in Excel or headless LibreOffice (`libreoffice --headless --convert-to xlsx out.xlsx`) and confirm formula cells show numbers, not `0`/`#REF!`/text. If totals are `0`, the inputs were strings — go back to step 2.
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: caching-strategy
3
+ description: Designs caching layers (cache-aside, write-through, TTLs, invalidation, stampede protection) typically with Redis to cut latency and database load when responses are slow or repeated.
4
+ when_to_use: User wants to add or fix caching, choose a cache pattern, set TTLs, solve stale data, cache stampede, or thundering-herd problems, or reduce DB load. NOT for measuring where time goes (use performance-profiling).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when a read is **slow or repeated** and the underlying data is **more read than written** and **tolerant of some staleness**.
10
+
11
+ - Use when: hot read path hits the DB/external API every request; identical responses recomputed; DB CPU/connection pool saturated by reads; p99 latency dominated by one query/call.
12
+ - Do NOT use when: you don't yet know *where* time goes (profile first), data must be strictly fresh on every read (auth tokens, balances, inventory-at-checkout), write-heavy with near-zero re-reads (cache churns, hit ratio stays low), or working set fits in one cheap query already.
13
+
14
+ Decision gate before writing any code, answer all four:
15
+ 1. What exact key identifies this read? (must be deterministic from request inputs)
16
+ 2. How stale can the value be — seconds, minutes, hours? (sets TTL ceiling)
17
+ 3. What's the current read:write ratio on this data? (<5:1 → caching rarely pays)
18
+ 4. What's the blast radius if a stale value is served? (decides invalidation rigor)
19
+
20
+ If you can't answer #2 and #4, stop — caching here risks a correctness bug, not a speedup.
21
+
22
+ ## Steps
23
+
24
+ 1. **Pick exactly one hot read to cache first.** Confirm it's read-heavy and idempotent (same inputs → same output). Resist caching writes or per-user-unique reads with no reuse.
25
+
26
+ 2. **Design the key namespace.** Format: `{domain}:{entity}:{id}:{version}` e.g. `user:profile:42:v3`. Rules:
27
+ - Include a schema/serializer **version segment** so a deploy with a changed shape can't read old bytes as the new type.
28
+ - Never put unbounded/unhashed user input in the key (cardinality explosion + injection); hash long or composite keys.
29
+ - Keep one key = one entity. Avoid baking query params you'll later need to invalidate independently into a single blob.
30
+
31
+ 3. **Choose the pattern:**
32
+ - **Cache-aside (default, start here):** read → on miss, load from source, write to cache with TTL, return. App owns cache; source stays the truth.
33
+ - **Write-through:** on write, update source **then** cache synchronously. Use when reads must reflect writes immediately and you accept slower writes.
34
+ - **Write-behind:** buffer writes, flush to source async. Only with a durable queue and an accepted data-loss window — most teams should not.
35
+ - Default to cache-aside unless a concrete requirement forces the others.
36
+
37
+ 4. **Set TTL deliberately, never "just put a number."** TTL = max acceptable staleness from step 2. Add **jitter**: store with `ttl ± rand(0..ttl*0.1)` so keys created together don't all expire on the same tick. Pick eviction policy explicitly (e.g. `allkeys-lru` for a pure cache, `volatile-ttl` if the instance also holds non-expiring data) — don't inherit `noeviction`, which turns a full cache into write errors.
38
+
39
+ 5. **Invalidate on write, don't rely on TTL alone for correctness.** On every mutation of the source, in the same transaction boundary: **delete** the affected key(s) (don't try to update them — update-in-place races with concurrent reads). For derived/list keys, track an index of dependent keys or use a key prefix + versioned namespace you can bump. After a DB commit, delete; if the delete can fail independently of the commit, prefer the **delete-after-commit + short TTL safety net** so a missed invalidation self-heals.
40
+
41
+ 6. **Add stampede protection before it bites in prod.** When a hot key expires under load, all callers miss at once and dogpile the source. Use one of:
42
+ - **Single-flight / per-key lock:** first miss acquires a short-lived lock (`SET lock:{key} 1 NX EX 5`), recomputes, fills cache; others briefly wait/retry or serve stale.
43
+ - **Stale-while-revalidate:** store `{value, soft_expiry, hard_expiry}`; serve stale past soft_expiry while one worker refreshes in background. Best UX for read-heavy paths.
44
+ - **Jittered TTL** (step 4) handles synchronized expiry; SWR/lock handles a single hot key. Use both.
45
+
46
+ 7. **Layer only if measured need.** Optional L1 in-process cache (small, short TTL, e.g. 1–5s) in front of L2 (Redis) for ultra-hot keys. Cap L1 size and remember L1 is **per-instance → harder to invalidate**; keep its TTL short enough that staleness from a missed L1 invalidation is tolerable.
47
+
48
+ 8. **Serialize compactly and cap size.** Pick a stable serializer (JSON for debuggability, msgpack/protobuf for size/speed). Reject caching values over a sane ceiling (e.g. a few hundred KB) — large values blow network and memory; cache an id/handle and fetch the body separately instead.
49
+
50
+ 9. **Verify (step below) — never declare done on "it returns faster once."**
51
+
52
+ ## Common Errors
53
+
54
+ - **Caching null/error as if it were data.** A miss that returns "not found" or throws must be cached **differently** (short negative-TTL sentinel) or not at all — otherwise either you re-hammer the DB for every missing id, or you pin a transient error for the full TTL.
55
+ - **Update-in-place on invalidation.** Reader loads old value → writer updates DB → writer overwrites cache → reader writes its stale value back. **Always delete, never set, on invalidation.** And delete *after* the source commit, not before.
56
+ - **Cache stampede ignored until launch.** Works fine in dev (1 caller), melts the DB on a hot key at scale. Add jitter + single-flight/SWR up front for any key you expect to be hot.
57
+ - **Unbounded key cardinality.** Caching per-request keys with no reuse (full query strings, timestamps, paginated infinite scroll) → near-zero hit ratio + memory bloat. Verify reuse exists before caching.
58
+ - **No serializer version in key.** Deploy changes the struct shape; new code deserializes old bytes → crash or silent corruption. Version segment in the key (step 2) prevents it.
59
+ - **`noeviction` on a cache instance.** When memory fills, writes start erroring instead of evicting cold keys, cascading into request failures. Set an `allkeys-*` / `volatile-*` policy.
60
+ - **TTL = correctness crutch.** Long TTL "to be safe" serves stale data; short TTL "to be fresh" kills hit ratio and re-stampedes. TTL is the staleness budget; explicit invalidation is the correctness mechanism. Use both.
61
+ - **Caching strongly-consistent data.** Balances, inventory at checkout, permission checks, auth state — do not cache, or cache only with write-through + immediate invalidation and a hard correctness review.
62
+ - **Thundering herd on cold start / flush.** After a cache restart or mass eviction, everything misses at once. Warm critical keys on deploy, or rely on per-key locks so only one caller rebuilds each key.
63
+
64
+ ## Verify
65
+
66
+ Caching is "done" only when measured, not when it "felt faster."
67
+
68
+ 1. **Hit ratio:** instrument hits/misses per key namespace; a working cache settles at a high hit ratio (target depends on path; a hot read should be well north of 80%). Persistently low → wrong key, no reuse, or TTL too short. Caching was the wrong move there.
69
+ 2. **Latency:** compare p50 **and p99** of the cached path before/after under realistic concurrency, not a single warm call. Confirm the source load (DB QPS / external calls) actually dropped — that's the real win.
70
+ 3. **Correctness under write:** write a test that (a) reads (populates cache), (b) mutates the source, (c) reads again and asserts the new value — proves invalidation fires. Add a test that a missing id doesn't repeatedly hit the source (negative caching) and doesn't pin an error.
71
+ 4. **Stampede:** fire N concurrent requests at a freshly-expired hot key; assert the source is hit ~once, not N times (single-flight/SWR working).
72
+ 5. **Eviction/memory:** confirm the eviction policy is set and the instance evicts rather than erroring when full; watch memory under load.
73
+ 6. **Failure mode:** kill/disconnect the cache and confirm the app **degrades to the source** (slower but correct), not 500s. The cache is an optimization, not a dependency.
74
+
75
+ If hit ratio is low or latency didn't move, the data wasn't cacheable here — revert and profile instead.