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.
- package/.env.example +23 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +201 -0
- package/README.md +239 -0
- package/dist/agentContext.js +2 -0
- package/dist/approval.js +78 -0
- package/dist/bin.js +461 -0
- package/dist/brain.js +186 -0
- package/dist/commands.js +66 -0
- package/dist/compaction.js +85 -0
- package/dist/config.js +101 -0
- package/dist/cost.js +59 -0
- package/dist/diff.js +36 -0
- package/dist/gateway/auth.js +32 -0
- package/dist/gateway/ledger.js +94 -0
- package/dist/gateway/lock.js +114 -0
- package/dist/gateway/schedule.js +74 -0
- package/dist/gateway/scheduler.js +87 -0
- package/dist/gateway/serve.js +57 -0
- package/dist/gateway/server.js +94 -0
- package/dist/gateway/telegram.js +115 -0
- package/dist/git.js +55 -0
- package/dist/hooks.js +104 -0
- package/dist/knowledge.js +68 -0
- package/dist/loop.js +169 -0
- package/dist/mcp.js +191 -0
- package/dist/memory.js +108 -0
- package/dist/providers/codex.js +86 -0
- package/dist/providers/keys.js +37 -0
- package/dist/providers/models.js +55 -0
- package/dist/providers/registry.js +241 -0
- package/dist/session.js +36 -0
- package/dist/skill-install.js +190 -0
- package/dist/skills.js +111 -0
- package/dist/tools/bash.js +26 -0
- package/dist/tools/edit.js +107 -0
- package/dist/tools/git.js +68 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/list.js +24 -0
- package/dist/tools/permission.js +30 -0
- package/dist/tools/read.js +18 -0
- package/dist/tools/recall.js +12 -0
- package/dist/tools/remember.js +14 -0
- package/dist/tools/schedule.js +61 -0
- package/dist/tools/search.js +54 -0
- package/dist/tools/skill.js +65 -0
- package/dist/tools/task.js +46 -0
- package/dist/tools/util.js +5 -0
- package/dist/tools/write.js +27 -0
- package/dist/ui/app.js +132 -0
- package/dist/ui/banner.js +20 -0
- package/dist/ui/brain-wizard.js +29 -0
- package/dist/ui/render.js +57 -0
- package/dist/ui/setup.js +46 -0
- package/package.json +77 -0
- package/second-brain/AGENTS.md +18 -0
- package/second-brain/CLAUDE.md +96 -0
- package/second-brain/Evals/retrieval-eval.md +30 -0
- package/second-brain/GEMINI.md +15 -0
- package/second-brain/Home.md +33 -0
- package/second-brain/README.md +29 -0
- package/second-brain/Runbooks/ingest-quarantine.md +27 -0
- package/second-brain/Runbooks/sleep-time-consolidation.md +26 -0
- package/second-brain/Shared/AI-Context-Index.md +52 -0
- package/second-brain/Shared/Core-Facts/protected-facts.md +21 -0
- package/second-brain/Shared/Decision-Memory/decision-log.md +24 -0
- package/second-brain/Shared/Memory-Inbox/memory-inbox.md +23 -0
- package/second-brain/Shared/Operating-State/current-state.md +30 -0
- package/second-brain/Shared/Provenance/ingest-log.md +27 -0
- package/second-brain/Shared/Rules/context-assembly-policy.md +28 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +33 -0
- package/second-brain/Shared/Rules/skills-admission.md +30 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +25 -0
- package/second-brain/Templates/bug.md +22 -0
- package/second-brain/Templates/handoff.md +21 -0
- package/second-brain/Templates/project.md +24 -0
- package/second-brain/Templates/session.md +26 -0
- package/second-brain/USER.md +36 -0
- package/second-brain/Vault Structure Map.md +106 -0
- package/skills/agent-tool-mcp-builder/SKILL.md +88 -0
- package/skills/api-design-review/SKILL.md +70 -0
- package/skills/async-concurrency-correctness/SKILL.md +93 -0
- package/skills/audit-accessibility-wcag/SKILL.md +59 -0
- package/skills/audit-technical-seo/SKILL.md +62 -0
- package/skills/auth-jwt-session/SKILL.md +88 -0
- package/skills/brainstorm-design/SKILL.md +73 -0
- package/skills/build-etl-pipeline/SKILL.md +58 -0
- package/skills/build-form-validation/SKILL.md +103 -0
- package/skills/build-office-docs/SKILL.md +80 -0
- package/skills/build-react-component/SKILL.md +116 -0
- package/skills/build-spreadsheet/SKILL.md +106 -0
- package/skills/caching-strategy/SKILL.md +75 -0
- package/skills/cicd-pipeline-author/SKILL.md +65 -0
- package/skills/cloud-cost-optimize/SKILL.md +91 -0
- package/skills/code-comments/SKILL.md +52 -0
- package/skills/code-review/SKILL.md +61 -0
- package/skills/db-migration-safety/SKILL.md +67 -0
- package/skills/debug-frontend-browser/SKILL.md +58 -0
- package/skills/debug-root-cause/SKILL.md +54 -0
- package/skills/dependency-upgrade/SKILL.md +56 -0
- package/skills/deploy-release/SKILL.md +64 -0
- package/skills/diff-table-parity/SKILL.md +58 -0
- package/skills/dockerfile-optimize/SKILL.md +82 -0
- package/skills/error-message/SKILL.md +58 -0
- package/skills/estimate-work/SKILL.md +54 -0
- package/skills/explore-codebase/SKILL.md +73 -0
- package/skills/git-commit-pr/SKILL.md +65 -0
- package/skills/gitops-deploy-workflow/SKILL.md +97 -0
- package/skills/implement-from-design/SKILL.md +69 -0
- package/skills/incident-response-sre/SKILL.md +78 -0
- package/skills/k8s-debug-workload/SKILL.md +135 -0
- package/skills/k8s-manifest-review/SKILL.md +86 -0
- package/skills/llm-eval-harness/SKILL.md +63 -0
- package/skills/manage-client-server-state/SKILL.md +94 -0
- package/skills/mermaid-diagram/SKILL.md +61 -0
- package/skills/message-queue-jobs/SKILL.md +139 -0
- package/skills/naming-helper/SKILL.md +57 -0
- package/skills/observability-instrument/SKILL.md +113 -0
- package/skills/optimize-core-web-vitals/SKILL.md +75 -0
- package/skills/optimize-sql-query/SKILL.md +67 -0
- package/skills/performance-profiling/SKILL.md +65 -0
- package/skills/process-pdf/SKILL.md +107 -0
- package/skills/profile-dataset/SKILL.md +97 -0
- package/skills/prompt-engineering/SKILL.md +70 -0
- package/skills/rag-pipeline/SKILL.md +53 -0
- package/skills/rate-limiting/SKILL.md +96 -0
- package/skills/refactor-cleanup/SKILL.md +54 -0
- package/skills/regex-build/SKILL.md +72 -0
- package/skills/release-notes/SKILL.md +79 -0
- package/skills/rest-graphql-contract/SKILL.md +71 -0
- package/skills/scrape-structured-web-data/SKILL.md +61 -0
- package/skills/secrets-management/SKILL.md +96 -0
- package/skills/security-review/SKILL.md +62 -0
- package/skills/shell-script-robust/SKILL.md +71 -0
- package/skills/style-responsive-tailwind/SKILL.md +70 -0
- package/skills/terraform-plan-review/SKILL.md +95 -0
- package/skills/type-safety-strict/SKILL.md +82 -0
- package/skills/validate-data-quality/SKILL.md +62 -0
- package/skills/wrangle-tabular-data/SKILL.md +75 -0
- package/skills/write-adr/SKILL.md +75 -0
- package/skills/write-analytical-sql/SKILL.md +71 -0
- package/skills/write-data-viz/SKILL.md +58 -0
- package/skills/write-docs/SKILL.md +54 -0
- package/skills/write-plan/SKILL.md +59 -0
- package/skills/write-playwright-e2e/SKILL.md +86 -0
- package/skills/write-prd/SKILL.md +65 -0
- package/skills/write-rfc/SKILL.md +75 -0
- package/skills/write-tests/SKILL.md +50 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terraform-plan-review
|
|
3
|
+
description: Reviews Terraform/OpenTofu plans and modules with a safety-first, diagnose-first lens — blast radius of destroys/replaces, identity churn, drift, state risks, secret exposure — before any apply. Triggers when reviewing a terraform plan diff, authoring modules, or gating an apply in CI.
|
|
4
|
+
when_to_use: ก่อน terraform/tofu apply, รีวิว plan diff, เขียน/ตรวจ module, มี resource destroy/replace น่ากลัว, สงสัย drift
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use this skill BEFORE any `terraform apply` / `tofu apply`, and whenever you:
|
|
10
|
+
- Review a plan diff (the `~`/`-`/`-/+` lines, or `terraform show -json` output).
|
|
11
|
+
- Author or refactor a module (variables, `for_each`/`count`, outputs).
|
|
12
|
+
- Gate an apply in CI/CD (PR check, plan-as-artifact, OPA/Sentinel policy).
|
|
13
|
+
|
|
14
|
+
Core stance: **diagnose first, never auto-approve.** A plan that only adds (`+`) is low-risk; one with `-` (destroy) or `-/+` (replace) on stateful resources can be irreversible data loss. Your job is to surface blast radius and force a conscious go/no-go — not to rubber-stamp green output. Treat the plan as the source of truth, ground every claim in provider docs, and never invent resource behavior.
|
|
15
|
+
|
|
16
|
+
The commands below apply identically to OpenTofu — swap `terraform` for `tofu`.
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
1. **Generate a deterministic, machine-readable plan.** Never review live console scroll. Run:
|
|
21
|
+
```
|
|
22
|
+
terraform plan -out=tfplan.bin -lock-timeout=120s
|
|
23
|
+
terraform show -json tfplan.bin > tfplan.json
|
|
24
|
+
```
|
|
25
|
+
Reviewing a saved plan file and then `apply tfplan.bin` guarantees what you reviewed is exactly what runs (no TOCTOU drift between plan and apply).
|
|
26
|
+
|
|
27
|
+
2. **Bucket every change by action.** Parse `tfplan.json` → `.resource_changes[].change.actions`:
|
|
28
|
+
- `["create"]` → low risk.
|
|
29
|
+
- `["update"]` → in-place, usually safe; check WHICH attributes (step 4).
|
|
30
|
+
- `["delete","create"]` (shown as `-/+`) → **replace** = destroy then recreate. High risk.
|
|
31
|
+
- `["create","delete"]` → replace with `create_before_destroy` (less downtime, but check capacity/quota).
|
|
32
|
+
- `["delete"]` → **destroy**. Highest risk. Flag every single one.
|
|
33
|
+
Quick triage:
|
|
34
|
+
```
|
|
35
|
+
jq -r '.resource_changes[] | select(.change.actions != ["no-op"]) | "\(.change.actions | join(",")) \(.address)"' tfplan.json | sort | uniq -c
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. **Flag blast radius — these block go by default:**
|
|
39
|
+
- **Any destroy/replace of stateful resources**: databases, disks/volumes, buckets/blob storage, stateful sets, persistent caches. Replacing these = data loss unless a snapshot/backup exists. Demand a backup or `prevent_destroy` before approving.
|
|
40
|
+
- **Forced replace from immutable attribute changes** — look at `change.replace_paths` in the JSON; it names the exact attribute forcing recreation (e.g. changing an availability zone, name, or engine version). Confirm the change is intended, not an accidental edit.
|
|
41
|
+
- **`count` → `for_each` migrations or reordered `count` lists** → index churn: removing element 0 of a `count` list shifts every later index, destroying/recreating unrelated resources. `for_each` with stable string keys avoids this. If you see a wide swath of replaces from one small edit, this is almost always the cause.
|
|
42
|
+
- **`-target` used to scope the apply** → silently skips dependency graph; flag as partial apply that can leave state inconsistent. Allowed only as a deliberate break-glass, noted in the PR.
|
|
43
|
+
- **`null_resource`/`terraform_data` with `triggers` churning** → re-runs provisioners; check the provisioner isn't destructive.
|
|
44
|
+
|
|
45
|
+
4. **Walk the failure-mode table (diagnose, don't assume):**
|
|
46
|
+
|
|
47
|
+
| Failure mode | What to grep / inspect | Why it bites |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **Identity / IAM churn** | replaces or deletes on roles, policies, bindings, service accounts, key pairs | Recreating identity revokes live access → cascading outages; new keys break dependents mid-apply |
|
|
50
|
+
| **Secret exposure** | `sensitive` values surfacing in outputs, `local_file`/`template_file` writing secrets to disk, secrets in plain `variable` defaults or committed `.tfvars`, plan JSON containing raw credentials | Plaintext secrets land in state, logs, CI artifacts, or git — state is NOT encrypted by default |
|
|
51
|
+
| **Blast radius** | count of destroy+replace vs intended scope; one-line edit → many resources changing | A typo in a shared `local`/module input can ripple across an entire environment |
|
|
52
|
+
| **CI drift** | `terraform plan -detailed-exitcode` returns 2 (changes) on a "no-change" branch; resources changed outside Terraform | State no longer matches reality; next apply may revert manual fixes or fail mid-run |
|
|
53
|
+
| **State corruption / lock** | missing or stale state lock, two pipelines applying same state, local state instead of remote backend, no versioning on the state bucket | Concurrent applies corrupt state; lost state = orphaned real infra you can no longer manage |
|
|
54
|
+
|
|
55
|
+
5. **Inspect module structure & state hygiene (when authoring or auditing):**
|
|
56
|
+
- Variables typed and validated (`type`, `validation` blocks); no untyped `any` on security-relevant inputs.
|
|
57
|
+
- Outputs don't leak secrets; mark with `sensitive = true` where needed (note: this only redacts CLI output, NOT state).
|
|
58
|
+
- **Remote state with locking + encryption + versioning** is mandatory for shared environments — local `terraform.tfstate` in a team repo is a finding. Confirm `backend` config locks (DynamoDB table / native lock / blob lease) and the state store has versioning so you can roll back.
|
|
59
|
+
- **Workspace / environment isolation**: prod and non-prod must not share one state file or one backend key. Verify per-env state separation.
|
|
60
|
+
- Pin provider and module versions (`required_providers` with `~>` constraints, module `version`/ref). Unpinned `latest` = non-reproducible plans.
|
|
61
|
+
|
|
62
|
+
6. **Ground every resource-behavior claim in provider docs (anti-hallucination).** Before asserting "this attribute forces replacement" or "this is safe in-place," confirm against the actual provider documentation for that resource/version. Do not infer behavior from the resource name. If you cannot verify, say so and mark the finding as uncertain rather than guessing.
|
|
63
|
+
|
|
64
|
+
7. **Run static security/policy scans and read the findings:**
|
|
65
|
+
```
|
|
66
|
+
tfsec . --soft-fail # or: trivy config .
|
|
67
|
+
checkov -d . --compact
|
|
68
|
+
```
|
|
69
|
+
Triage real issues (public exposure, unencrypted storage, permissive IAM, open security groups) vs. accepted/false-positive rules. Don't dump raw scanner output — summarize what actually matters for this change.
|
|
70
|
+
|
|
71
|
+
8. **Emit a go / no-go verdict.** Structure: counts by action → list every destroy/replace with its `replace_paths` cause → secret/IAM findings → scan findings → explicit **GO** or **NO-GO** with the single most dangerous line called out, and the exact remediation (snapshot first / add `prevent_destroy` / switch to `for_each` / pull secret into a vault). Never end with just "looks fine."
|
|
72
|
+
|
|
73
|
+
## Common Errors
|
|
74
|
+
|
|
75
|
+
- **Reviewing live `plan` output, then running a fresh `apply`.** State or remote data can change in between → you apply something you never reviewed. Always `-out` a plan file and apply that exact file.
|
|
76
|
+
- **Trusting green/“no changes” without `-detailed-exitcode`.** Plain `plan` exits 0 even when there are changes. Use `-detailed-exitcode`: `0` = no changes, `1` = error, `2` = changes present. CI gates must check for `2`.
|
|
77
|
+
- **Missing replaces because you only read `+`/`-` summary lines.** A `-/+` (replace) on a database reads almost like a normal update at a glance but destroys data. Parse `actions` from JSON, don't eyeball the colored summary.
|
|
78
|
+
- **Assuming `sensitive = true` protects the secret.** It only redacts CLI/log output. The value is still **stored in plaintext in state** and may appear in plan JSON artifacts. The fix is to not put the secret in Terraform-managed values at all — reference a secrets manager.
|
|
79
|
+
- **Editing a `count`-based list and triggering mass recreation.** Inserting/removing a middle element shifts all later indices → cascade of destroy/replace. Migrate to `for_each` with stable keys; expect the migration plan itself to churn (use `moved {}` blocks to avoid destroy on refactor).
|
|
80
|
+
- **Approving a `-target` apply as if it were a full plan.** `-target` bypasses parts of the dependency graph and can leave state half-applied. It is break-glass only.
|
|
81
|
+
- **Refactoring modules/renaming resources without `moved {}` blocks.** Renaming an address makes Terraform plan destroy-old + create-new instead of a no-op rename. Add `moved {}` (or `import`/`state mv`) so refactors stay free of real destroys.
|
|
82
|
+
- **Local state in a shared repo / no lock.** Two applies race and corrupt state. Require a remote backend with locking, versioning, and encryption before approving.
|
|
83
|
+
|
|
84
|
+
## Verify
|
|
85
|
+
|
|
86
|
+
A review is complete only when ALL of these hold:
|
|
87
|
+
- [ ] Action buckets counted from `tfplan.json` (create / update / replace / destroy) — not eyeballed from console color.
|
|
88
|
+
- [ ] **Every** destroy and replace is listed with its `replace_paths` root cause and an explicit data-loss assessment.
|
|
89
|
+
- [ ] Stateful destroys/replaces have a verified backup/snapshot OR `prevent_destroy`, OR the verdict is NO-GO.
|
|
90
|
+
- [ ] Plan JSON, outputs, and `.tfvars` checked for plaintext secrets; remote state confirmed encrypted + versioned + locked.
|
|
91
|
+
- [ ] `terraform plan -detailed-exitcode` interpreted correctly in any CI gate (treats exit `2` as "changes present").
|
|
92
|
+
- [ ] `tfsec`/`checkov` (or equivalent) run; findings triaged, not ignored.
|
|
93
|
+
- [ ] Every resource-behavior claim grounded in provider docs; uncertain points flagged, not asserted.
|
|
94
|
+
- [ ] Output ends with an explicit **GO / NO-GO** plus the top risk line and its remediation.
|
|
95
|
+
- [ ] The reviewed artifact is the same plan file that gets applied (`apply tfplan.bin`), so there's no gap between review and execution.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: type-safety-strict
|
|
3
|
+
description: Enforces strict static typing in TypeScript and Python (and hardens Rust/Go signatures), removing any/escape hatches and modeling state with precise types when code is loosely typed or fails the type checker.
|
|
4
|
+
when_to_use: User wants to eliminate 'any', pass strict mode / mypy / pyright, model domain state with unions/generics, add type hints, or fix type errors. NOT for runtime logic bugs (use debug-root-cause) or restructuring (use refactor-cleanup).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Trigger this skill when code is loosely typed or fails the type checker and the goal is type correctness, not behavior change:
|
|
10
|
+
|
|
11
|
+
- "Remove `any`", "make it pass strict / mypy --strict / pyright strict", "add type hints", "stop using `# type: ignore`".
|
|
12
|
+
- Modeling domain state precisely: a value that's "one of N shapes", a function whose return depends on its args, IDs that must not be mixed up.
|
|
13
|
+
- A type checker is red and you need it green **without** suppressions.
|
|
14
|
+
|
|
15
|
+
Do NOT use when:
|
|
16
|
+
- The bug is runtime/logic (wrong output, crash, race) and types already check → use `debug-root-cause`.
|
|
17
|
+
- The goal is moving/renaming/deduplicating code structure → use `refactor-cleanup`.
|
|
18
|
+
|
|
19
|
+
Guardrail: this skill changes **types and boundaries only**. If a fix requires changing runtime behavior, stop and flag it — don't silently alter logic to satisfy the checker.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Baseline the checker, then flip strictness on.** Run the type checker first and save the error count — you compare against this at the end.
|
|
24
|
+
- TS: `tsconfig.json` → `"strict": true` plus `"noUncheckedIndexedAccess": true`, `"exactOptionalPropertyTypes": true`, `"noImplicitOverride": true`. Run `tsc --noEmit`.
|
|
25
|
+
- Python (pyright): `pyrightconfig.json` → `"typeCheckingMode": "strict"`. Or mypy: `[mypy] strict = true`, `warn_unused_ignores = true`, `disallow_any_generics = true`. Run `pyright` / `mypy <pkg>`.
|
|
26
|
+
- Turning strictness on usually *increases* errors first. That's expected — work the list down to zero.
|
|
27
|
+
|
|
28
|
+
2. **Inventory the escape hatches.** Find every loophole before fixing anything:
|
|
29
|
+
- TS: grep for `: any`, `as any`, `as unknown as`, `@ts-ignore`, `@ts-expect-error`, `Function`, `object`, `{}` as a type, non-null `!`.
|
|
30
|
+
- Python: grep for `Any`, `# type: ignore`, `cast(`, bare `dict`/`list`/`tuple` without params, `object` params, untyped `def`.
|
|
31
|
+
- Each one is a TODO. The end state has zero of these unless individually justified with a one-line comment explaining *why* it's unavoidable.
|
|
32
|
+
|
|
33
|
+
3. **Replace `any`/`Any` with the real type; narrow `unknown` at the edge.** Never widen to silence — narrow to truth.
|
|
34
|
+
- Prefer `unknown` over `any` when the type is genuinely not known yet, then narrow with guards before use.
|
|
35
|
+
- TS guards: `typeof x === "string"`, `Array.isArray`, `"key" in obj`, custom `function isFoo(x: unknown): x is Foo`.
|
|
36
|
+
- Python guards: `isinstance`, `TypeGuard`/`TypeIs` predicates, `assert x is not None` to drop `Optional`.
|
|
37
|
+
- If a value comes from `JSON.parse`, an API, env, or `**kwargs`, its static type is `unknown`/`Any` until validated (step 5) — don't assert it away.
|
|
38
|
+
|
|
39
|
+
4. **Model state with precise types instead of loose bags.** This is where loose typing actually gets fixed:
|
|
40
|
+
- **"One of N shapes" → discriminated union.** TS: `type Result<T> = { ok: true; value: T } | { ok: false; error: E }`, each arm sharing a literal tag field. Python: `Literal`-tagged `TypedDict` union, or a `Union` of frozen dataclasses + `match`.
|
|
41
|
+
- **Return type depends on args → generics**, not overloaded `any`. TS: `function first<T>(xs: T[]): T | undefined`. Python: `TypeVar` + `Generic[T]`.
|
|
42
|
+
- **Fixed set of string/number values → literal/enum**, not `string`. TS: `type Status = "open" | "closed"`. Python: `Literal["open","closed"]` or `enum.Enum`.
|
|
43
|
+
- **Frozen config / lookup keys → `as const`** (TS) so keys/values infer as literals, not widened.
|
|
44
|
+
- **IDs that must not be swapped → branded/newtype.** TS: `type UserId = string & { readonly __brand: "UserId" }`. Python: `UserId = NewType("UserId", str)`. Catches "passed orderId where userId expected" at compile time.
|
|
45
|
+
- **Structural contracts, not concrete classes** for params → TS `interface`, Python `Protocol`. Type the *shape you use*, not the *class you import*.
|
|
46
|
+
|
|
47
|
+
5. **Validate every external boundary at runtime, and derive the static type from the schema.** Static types are erased at runtime; data crossing a boundary (HTTP body, DB row, env, CLI args, file, message queue) is `unknown` until proven.
|
|
48
|
+
- TS: define a `zod`/`valibot` schema, `parse()` at the boundary, and use `z.infer<typeof Schema>` as *the* type — one source of truth, schema and type can't drift.
|
|
49
|
+
- Python: `pydantic` model `.model_validate()` at the boundary; the model class *is* the type.
|
|
50
|
+
- Anti-pattern to delete on sight: `const body = req.body as RequestBody` / `cast(RequestBody, payload)` — that's a lie to the compiler, not validation.
|
|
51
|
+
|
|
52
|
+
6. **Tighten signatures and force exhaustiveness.** Make the function contract say exactly what it accepts and returns:
|
|
53
|
+
- Add explicit return types to exported/public functions (don't rely on inference at API boundaries).
|
|
54
|
+
- Narrow params from `any`/`object`/`dict` to the real shape; prefer `readonly`/immutable params where nothing is mutated.
|
|
55
|
+
- Add an exhaustiveness check on every union switch so a *new* variant becomes a compile error, not a silent fall-through:
|
|
56
|
+
- TS: `default: const _exhaustive: never = x; throw new Error(...)`.
|
|
57
|
+
- Python: in the final `else`/`case _:`, assign to a function typed `def assert_never(x: Never) -> Never`.
|
|
58
|
+
|
|
59
|
+
7. **Drive errors to zero with no suppressions, iterating.** Re-run the checker after each cluster of fixes. Resolve at the root (fix the type) — do **not** add `as any` / `# type: ignore` / loosen the config to turn it green. Any surviving suppression must be a last resort, scoped to the narrowest line, with a comment stating why no real type exists.
|
|
60
|
+
|
|
61
|
+
## Common Errors
|
|
62
|
+
|
|
63
|
+
- **Widening to silence the checker.** `as any`, `cast(...)`, `# type: ignore`, or relaxing `tsconfig`/`mypy` to clear errors. This makes the checker green while the code stays unsafe — strictly worse than before, because now the lie is invisible. Narrow instead of widen.
|
|
64
|
+
- **Casting where you should validate.** `req.body as User` / `cast(User, json)` asserts a type the compiler can't verify; the first malformed payload is an undebuggable runtime crash. Boundaries need runtime schema validation (step 5), not a cast.
|
|
65
|
+
- **`!` / `assert x is not None` to kill `Optional` without proving it.** TS non-null `!` and Python blind asserts re-introduce the null bug the type system just caught. Narrow with a real check, or make the type non-optional upstream.
|
|
66
|
+
- **`noUncheckedIndexedAccess` surprises.** With it on, `arr[i]` and `record[key]` are `T | undefined`. Don't blanket-`!` them — guard the access or use `.at()` / explicit `in` checks. This flag catches real out-of-bounds bugs.
|
|
67
|
+
- **Schema and type drifting apart.** Hand-writing both a `zod` schema *and* a separate `interface` lets them diverge silently. Always derive the type from the schema (`z.infer`), never maintain two copies.
|
|
68
|
+
- **`as const` forgotten on lookup tables.** Without it, `{ a: "x" }` widens `a` to `string`, losing literal keys/values and breaking exhaustiveness. Add `as const`.
|
|
69
|
+
- **mypy passing on untyped code.** mypy treats unannotated functions as `Any`-bodied and skips them — green ≠ checked. Require annotations (`disallow_untyped_defs`) or use pyright strict, which flags missing annotations directly.
|
|
70
|
+
- **Generic params silently `any`.** `Array`, `Promise`, bare `dict`/`list`/`Callable` default their params to `any`/`Any` and pass strict by accident. Always parameterize: `Promise<Foo>`, `dict[str, int]`, `Callable[[int], str]`. (`disallow_any_generics` catches this.)
|
|
71
|
+
- **Rust/Go hardening, not just hint-adding.** For these the win is signature precision, not "any" removal: Rust → return `Result<T, E>`/`Option<T>` instead of panicking or sentinel values, use newtypes, avoid leaking `Box<dyn Any>`. Go → return explicit `error`, avoid `interface{}`/`any`, use typed constants over bare strings. Don't try to bolt a TS/Python type-checker workflow onto them.
|
|
72
|
+
|
|
73
|
+
## Verify
|
|
74
|
+
|
|
75
|
+
Done only when ALL hold:
|
|
76
|
+
|
|
77
|
+
1. **Checker is clean from a fresh run** — `tsc --noEmit` / `pyright` / `mypy --strict <pkg>` exits 0. Show the command and its output, not "it passes".
|
|
78
|
+
2. **Strict config is actually committed** — the diff includes the `tsconfig`/`pyrightconfig`/`mypy` flags being on. Green with strictness off is not a pass.
|
|
79
|
+
3. **Escape-hatch count went down, not up** — re-grep the patterns from step 2; new `any`/`as any`/`# type: ignore`/`cast` must be zero (or each individually justified by a comment). Compare against the step-1 baseline.
|
|
80
|
+
4. **Boundaries validate at runtime** — every external input path parses through a schema/model; no `as`/`cast` smuggling unvalidated data in.
|
|
81
|
+
5. **Exhaustiveness holds** — adding a fake variant to a modeled union produces a compile error (the `never`/`assert_never` fires). Spot-check one union.
|
|
82
|
+
6. **No behavior changed** — existing tests still pass unchanged; the diff is types/validation only. If runtime behavior had to change, it was flagged explicitly, not slipped in.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: validate-data-quality
|
|
3
|
+
description: Defines and runs rule-based data-quality checks — completeness, uniqueness, freshness, range, referential integrity — using Great Expectations-style assertion frameworks.
|
|
4
|
+
when_to_use: When the user wants to guard data correctness with explicit rules — assert no nulls/dupes in key columns, enforce value ranges or referential integrity, check freshness/timeliness, or wire data-quality gates into a pipeline that fail loudly.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use when the contract is known and you want to **assert** it, not discover it:
|
|
10
|
+
|
|
11
|
+
- Key columns must have **no nulls** / **no dupes** (PK or business key).
|
|
12
|
+
- Values must stay in a **range / enum / regex** (e.g. `price >= 0`, `status in (...)`, ISO date format).
|
|
13
|
+
- A child table must not reference **missing parent keys** (referential integrity), or a derived column must agree with its source (cross-column consistency).
|
|
14
|
+
- A table/feed must be **fresh** (max timestamp within SLA) or have an **expected row count** (volume not collapsed/exploded).
|
|
15
|
+
- You need a **gate** that blocks a bad load and exits non-zero in CI/the pipeline.
|
|
16
|
+
|
|
17
|
+
Distinct from exploratory profiling (`profile-dataset`): that one **describes** unknown data; this one **enforces** a known contract and fails. If you don't yet know the rules, profile first, then encode findings here as expectations.
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. **Derive expectations, don't guess.** Pull rules from: (a) the schema/DDL (NOT NULL, UNIQUE, FK, CHECK, types), (b) stated business rules, (c) a prior profile run for value distributions. Write each as one named assertion with an explicit expected value. Resist "looks fine" smoke tests — every column in scope gets at least one rule or an explicit decision to skip it.
|
|
22
|
+
|
|
23
|
+
2. **Pick the engine, keep it native to the data location.**
|
|
24
|
+
- Tabular / pandas / Spark, or you want a docs site + history → **Great Expectations** (`great_expectations`): build a `Validator` on a `Batch`, attach expectations, run a `Checkpoint`.
|
|
25
|
+
- Warehouse-resident data → push rules down as **SQL** (`dbt test`, or hand-written `COUNT(*) WHERE <violation>`) so you never extract the full table.
|
|
26
|
+
- Small/embedded → lightweight custom asserts (a list of `(name, fn, severity)` over the frame). Don't pull a heavy framework for 5 checks on a CSV.
|
|
27
|
+
|
|
28
|
+
3. **Map each rule to a check family + assertion:**
|
|
29
|
+
- Completeness → `expect_column_values_to_not_be_null` (or `SELECT count(*) WHERE col IS NULL`). Report **null %**, not just a boolean.
|
|
30
|
+
- Uniqueness → `expect_column_values_to_be_unique` / `expect_compound_columns_to_be_unique` for composite keys; or `GROUP BY key HAVING count(*) > 1`.
|
|
31
|
+
- Validity → `expect_column_values_to_be_between` (range), `expect_column_values_to_match_regex`, `expect_column_values_to_be_in_set` (enum), `expect_column_values_to_be_of_type`.
|
|
32
|
+
- Consistency / referential → `expect_column_pair_values_*`, or anti-join `child LEFT JOIN parent ... WHERE parent.key IS NULL`; cross-column `expect_multicolumn_sum_to_equal` / explicit predicate.
|
|
33
|
+
- Freshness / volume → `expect_column_max_to_be_between(now - SLA, now)` on the timestamp; `expect_table_row_count_to_be_between(min, max)` for volume drift.
|
|
34
|
+
|
|
35
|
+
4. **Set severity per rule: hard-fail vs threshold.** A broken PK/FK is a **hard fail** (block). A "warn" tolerance (e.g. `mostly=0.99`, allow 1% null in a soft column) is a **threshold** — log it loud but optionally non-blocking. Be explicit about which rules are which; default new rules to hard-fail until proven noisy.
|
|
36
|
+
|
|
37
|
+
5. **Emit an actionable report.** For every failure include: rule name, expected vs observed, **violation count + %**, and a **sample of offending rows / values** (cap at ~5–10, never dump the table). Output machine-readable (JSON) so the gate can parse it, plus a human summary line. Aggregate to one final `PASS`/`FAIL`.
|
|
38
|
+
|
|
39
|
+
6. **Wire it as a gate.** The runner must **exit non-zero on any hard-fail** so the pipeline stops before the bad data lands. Run validation on the **post-transform / pre-load** batch (a staging table), not after it's already committed to the target. In dbt, fail the build; in a script, `sys.exit(1)`.
|
|
40
|
+
|
|
41
|
+
7. **Root-cause guardrail (mandatory).** When a check fails, fix the **data or the upstream producer** — never relax the assertion, widen the range, bump `mostly` down, or comment out the test to go green. A weakened assertion is a silent production bug. If a rule is genuinely wrong, change it deliberately with a recorded reason, not to dodge a red run.
|
|
42
|
+
|
|
43
|
+
## Common Errors
|
|
44
|
+
|
|
45
|
+
- **`mostly` masks real breakage.** `mostly=0.95` lets 5% violations pass silently. Use it only for known-soft columns; PK/FK/critical fields get `mostly=1.0`.
|
|
46
|
+
- **Nulls slip past range/regex checks.** Most validity expectations **skip nulls** by design. A column can pass `be_between` and `match_regex` while full of nulls — pair every validity rule with an explicit completeness rule.
|
|
47
|
+
- **Float / numeric-type range checks false-fail.** Comparing `Decimal` vs `float`, or NaN (`NaN` is never `>= 0`), throws off `be_between`. Normalize dtypes and decide NaN policy before asserting.
|
|
48
|
+
- **Uniqueness on a single column when the key is composite.** `(date, user_id)` unique ≠ `user_id` unique. Use the compound expectation or the dupes pass undetected.
|
|
49
|
+
- **Timezone-naive freshness.** `now()` local vs UTC timestamps makes a fresh feed look stale (or vice versa). Compare in one explicit tz.
|
|
50
|
+
- **Referential check on the wrong direction / before the parent loads.** Validate the child after parents are present, and anti-join child→parent (not the reverse), or you'll flag valid rows.
|
|
51
|
+
- **Validating the whole warehouse table in pandas.** Pulling millions of rows to assert in memory is slow and OOMs — push the check down as SQL `COUNT WHERE violation`.
|
|
52
|
+
- **Gate that reports FAIL but still exits 0.** The pipeline keeps going and the bad load lands anyway. Verify the exit code, not just the printed verdict.
|
|
53
|
+
- **Stale expectation suite after a schema change.** A renamed/dropped column makes the rule error or silently no-op. Treat the suite as code: update it with the schema, version it.
|
|
54
|
+
|
|
55
|
+
## Verify
|
|
56
|
+
|
|
57
|
+
1. **Inject a violation, confirm it's caught.** Add a known null/dupe/out-of-range/orphan-FK row (or use a tampered fixture); the runner must report it **and exit non-zero**. A suite that never fails on bad data proves nothing.
|
|
58
|
+
2. **Confirm clean data passes** — run on a known-good batch and get `PASS` with exit 0 (guards against an over-strict rule that fails everything).
|
|
59
|
+
3. **Check the count math:** reported violation count = an independent `SELECT count(*) WHERE <violation>`. They must match.
|
|
60
|
+
4. **Inspect the offending-row sample** in the report — the rows shown actually violate the rule (not a formatting artifact).
|
|
61
|
+
5. **Test the gate in place:** run inside the pipeline/CI with bad data and confirm the downstream load is **blocked**, not just logged.
|
|
62
|
+
6. **Grep the diff for weakened assertions** — no newly-lowered `mostly`, widened ranges, or commented-out checks were added to make the run green.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wrangle-tabular-data
|
|
3
|
+
description: Cleans, transforms, joins, reshapes, and aggregates tabular data (CSV/Parquet/DataFrames) with pandas, handling missing values, type coercion, dedup, and time-series resampling.
|
|
4
|
+
when_to_use: When the user needs to clean or transform a dataset in code — fix missing/dirty values, coerce types, dedup, join/merge tables, pivot/melt/reshape, group-and-aggregate, or resample a time series, typically producing a cleaned CSV/DataFrame for downstream use.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use this when transforming a dataset **in code** and emitting a cleaned artifact (CSV/Parquet/DataFrame) for a downstream step.
|
|
10
|
+
|
|
11
|
+
Triggers: fix missing/dirty values, coerce dtypes, dedup, join/merge tables, pivot/melt/reshape, groupby-aggregate, rolling/window, or resample a time series.
|
|
12
|
+
|
|
13
|
+
Not this skill:
|
|
14
|
+
- Producing a formatted `.xlsx` for a human to open → use **build-spreadsheet** (this skill is a code-level transform, the output is data not presentation).
|
|
15
|
+
- Only inspecting/summarizing a dataset without changing it → use **profile-dataset** (transform vs. inspect).
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Load with explicit dtypes — never trust the inferrer.**
|
|
20
|
+
- CSV: `pd.read_csv(path, dtype={...}, parse_dates=[...], na_values=["", "NA", "null", "-", "N/A"], keep_default_na=True)`. Pass `dtype=str` for ID/code/zip columns to stop them becoming floats (`007` → `7.0`, leading zeros gone forever).
|
|
21
|
+
- Parquet: `pd.read_parquet(path)` already carries a schema — read it, don't re-coerce blindly.
|
|
22
|
+
- Capture `df.shape` and `df.dtypes` immediately; these are the baseline for the transformation log.
|
|
23
|
+
|
|
24
|
+
2. **Diagnose before touching anything.** Run `df.isna().sum()`, `df.nunique()`, and `df.dtypes`. For each `object` column you expect numeric/date, check why it's `object` (usually one stray non-numeric value or a thousands separator). Decide the strategy per column up front — do not clean ad hoc.
|
|
25
|
+
|
|
26
|
+
3. **Coerce types deliberately.**
|
|
27
|
+
- Numeric: `pd.to_numeric(s, errors="coerce")` then inspect the new NaNs (those are your dirty rows — log how many). Strip `$`, `,`, `%` first: `s.str.replace(r"[,$%]", "", regex=True)`.
|
|
28
|
+
- Datetime: `pd.to_datetime(s, errors="coerce", utc=True)` — force UTC so you never carry tz-naive timestamps into a resample/merge. If source is local time, localize then convert.
|
|
29
|
+
- Categorical: convert low-cardinality string columns with `.astype("category")` (memory + faster groupby).
|
|
30
|
+
|
|
31
|
+
4. **Clean values.**
|
|
32
|
+
- Missing: pick per column — `dropna(subset=[...])` for required keys, `fillna(value)` for known defaults, `ffill`/`bfill` only for ordered/time-series data. State which strategy and why in the log. Never blanket `fillna(0)` across the frame.
|
|
33
|
+
- Strings: `s.str.strip()`, normalize case (`.str.lower()`), collapse whitespace (`.str.replace(r"\s+", " ", regex=True)`) before dedup/join — invisible trailing spaces silently break key matches.
|
|
34
|
+
- Outliers: only act if asked; clip (`.clip(lower, upper)`) or flag, don't silently drop.
|
|
35
|
+
|
|
36
|
+
5. **Dedup with intent.** `df.duplicated(subset=[keys]).sum()` first to know the count. Then `df.drop_duplicates(subset=[keys], keep="first")`. Choosing `keep` matters when non-key columns differ — sort first if "latest wins" (`sort_values("ts").drop_duplicates(subset=keys, keep="last")`).
|
|
37
|
+
|
|
38
|
+
6. **Merge/join — always validate cardinality.** Use `validate=` to make pandas raise on a bad assumption: `df.merge(other, on="id", how="left", validate="m:1")`. Options: `1:1`, `1:m`, `m:1`, `m:m`. After merge, assert row count didn't explode (`assert len(out) <= len(left) * expected`). Check `indicator=True` to count unmatched keys (`how="left"` + `_merge == "left_only"`).
|
|
39
|
+
|
|
40
|
+
7. **Reshape.**
|
|
41
|
+
- Long→wide: `df.pivot_table(index=..., columns=..., values=..., aggfunc=...)` (use `pivot_table` not `pivot` so duplicate index/column pairs aggregate instead of raising).
|
|
42
|
+
- Wide→long: `df.melt(id_vars=[...], value_vars=[...], var_name=..., value_name=...)`.
|
|
43
|
+
- Aggregate: `df.groupby(keys, dropna=False).agg(out_col=("src", "func"))` — named aggregation keeps column names clean; `dropna=False` so NaN keys aren't silently dropped.
|
|
44
|
+
- Window: sort by the order column first, then `.rolling(window)` / `.expanding()` / `.shift()`.
|
|
45
|
+
|
|
46
|
+
8. **Time-series resample.** Index must be a tz-aware DatetimeIndex (`df.set_index("ts")`). Then `df.resample("1h").agg(...)` (downsample) or `.resample("1min").interpolate()` / `.asfreq()` (upsample). Per-group: `df.groupby("id").resample("1D").sum()`. Confirm the freq string is right (`"h"`, `"D"`, `"W"`, `"M"`, `"MS"` differ).
|
|
47
|
+
|
|
48
|
+
9. **Big files — stay in memory budget.**
|
|
49
|
+
- Read in chunks: `for chunk in pd.read_csv(path, chunksize=500_000): ...` and reduce per chunk.
|
|
50
|
+
- Set `dtype` + `category` at read time (biggest single win).
|
|
51
|
+
- Select columns with `usecols=[...]` — never load columns you won't use.
|
|
52
|
+
- If pandas still won't fit, switch the load+filter step to `pyarrow`/`polars` and hand a smaller frame back to pandas.
|
|
53
|
+
|
|
54
|
+
10. **Output + transformation log.** Write the artifact (`to_parquet` preferred — preserves dtypes; `to_csv(index=False)` if a CSV is required). Then emit a short log: rows in→out, columns dropped/added, dtype changes, NaNs coerced, duplicates removed, join match rate. This is the deliverable's audit trail, not optional.
|
|
55
|
+
|
|
56
|
+
## Common Errors
|
|
57
|
+
|
|
58
|
+
- **Silent 1:many join blow-up.** A left join on a non-unique right key multiplies rows. Symptom: output row count jumps, later aggregates double-count. Fix: always pass `validate="m:1"` (or the correct cardinality) so pandas raises instead of silently fanning out.
|
|
59
|
+
- **Mixed-type column read as `object`.** One `"N/A"` in a numeric column makes the whole column `object`; arithmetic then fails or coerces weirdly. Fix: `pd.to_numeric(errors="coerce")` and inspect the produced NaNs.
|
|
60
|
+
- **Leading zeros / big-int IDs corrupted on load.** `read_csv` infers `int64`/`float64` and drops leading zeros or rounds 19-digit IDs. Fix: `dtype=str` for any code/ID/zip/phone column.
|
|
61
|
+
- **Tz-naive timestamps breaking resample/merge.** Mixing naive and aware datetimes raises or aligns wrong across DST. Fix: `utc=True` on every `to_datetime`; localize source local-time explicitly.
|
|
62
|
+
- **`pivot` raising on duplicates.** `df.pivot` errors on duplicate index/column pairs. Fix: use `pivot_table` with an explicit `aggfunc`.
|
|
63
|
+
- **`groupby` silently dropping NaN keys.** Default `dropna=True` makes rows with NaN group keys vanish from the result. Fix: `groupby(..., dropna=False)` when NaN is a real category.
|
|
64
|
+
- **`fillna`/`ffill` on unsorted data.** Forward-fill before sorting propagates the wrong value. Fix: `sort_values` by the order/time column before any fill or rolling op.
|
|
65
|
+
- **Chained-assignment / `SettingWithCopyWarning`.** Edits on a slice don't stick. Fix: operate on `.copy()` or use `.loc[mask, col] = ...`.
|
|
66
|
+
|
|
67
|
+
## Verify
|
|
68
|
+
|
|
69
|
+
- Row count: `len(out)` matches expectation (joins didn't explode, dedup removed the diagnosed count, filters dropped what you intended).
|
|
70
|
+
- Dtypes: `out.dtypes` are final (no stray `object` where numeric/datetime expected); IDs still strings with leading zeros intact.
|
|
71
|
+
- Keys: `out.duplicated(subset=keys).sum() == 0` after dedup; join match rate logged (unmatched-key count is acceptable and known, not surprising).
|
|
72
|
+
- No accidental NaN: `out.isna().sum()` only shows NaNs you chose to keep.
|
|
73
|
+
- Time series: index is tz-aware DatetimeIndex, no gaps/duplicates in the resampled frequency.
|
|
74
|
+
- Round-trip: re-read the written artifact and confirm `shape` + `dtypes` survive (Parquet keeps them; CSV will re-infer — re-pass `dtype` if the CSV is the handoff format).
|
|
75
|
+
- Transformation log exists and reconciles in→out counts.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-adr
|
|
3
|
+
description: Sanook captures a significant technical decision as a structured Architecture Decision Record (Michael Nygard format) — context, decision, alternatives considered, consequences — written to docs/adr/ as numbered files with an index.
|
|
4
|
+
when_to_use: A decision moment appears ('let's go with X instead of Y', choosing a stack/library/pattern/boundary); user asks to 'record this decision' or 'write an ADR'; locking in an architectural choice.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Invoke when a decision is being locked in and would be expensive to reverse or confusing to re-litigate later:
|
|
10
|
+
|
|
11
|
+
- A choice was made between real options: "let's go with X instead of Y" — a stack, library, framework, datastore, protocol, pattern, or module boundary.
|
|
12
|
+
- User explicitly says "record this decision" / "write an ADR" / "ADR this".
|
|
13
|
+
- Reversing an earlier decision (the old ADR must be superseded, not silently deleted).
|
|
14
|
+
|
|
15
|
+
Do NOT use for: reversible implementation details, code style nits, or decisions with no real alternative (those go in code comments or commit messages, not an ADR). One ADR = one decision; if the prompt bundles several, split them.
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Name the decision and its trigger before writing.** State it as a single sentence: "We will use X for Y, because Z." If you can't, the decision isn't ripe — ask which option won and why the alternatives lost. Capture the trigger (the problem/forcing function that made a decision necessary) — that becomes Context. If no genuine alternative was ever on the table, stop: it's not an ADR.
|
|
20
|
+
|
|
21
|
+
2. **Locate or create `docs/adr/`.** Check whether the repo already has an ADR directory — common locations: `docs/adr/`, `docs/architecture/decisions/`, `doc/adr/`, `adr/`. Reuse the existing one and its filename convention; do NOT invent a parallel directory. If none exists, create `docs/adr/`.
|
|
22
|
+
|
|
23
|
+
3. **Assign the next sequential number.** List existing files (`ls docs/adr/`), find the highest `NNNN`, add 1. Zero-pad to 4 digits. Filename: `NNNN-kebab-case-title.md` (e.g. `0007-use-postgres-for-event-store.md`). Never reuse or renumber an existing ADR number — numbers are permanent IDs other ADRs link to.
|
|
24
|
+
|
|
25
|
+
4. **Write the record in Nygard format**, exactly these sections in order:
|
|
26
|
+
```markdown
|
|
27
|
+
# NNNN. <Short imperative title>
|
|
28
|
+
|
|
29
|
+
- Status: <Proposed | Accepted | Deprecated | Superseded by ADR-MMMM>
|
|
30
|
+
- Date: <YYYY-MM-DD>
|
|
31
|
+
|
|
32
|
+
## Context
|
|
33
|
+
<The forces at play: the problem, constraints, requirements, and assumptions
|
|
34
|
+
that make a decision necessary. Factual and neutral — no solution yet.>
|
|
35
|
+
|
|
36
|
+
## Decision
|
|
37
|
+
<"We will ..." — the choice, stated actively and unambiguously. One decision.>
|
|
38
|
+
|
|
39
|
+
## Alternatives Considered
|
|
40
|
+
- **<Option A>** — <what it is> — rejected because <concrete reason>.
|
|
41
|
+
- **<Option B>** — <what it is> — rejected because <concrete reason>.
|
|
42
|
+
<Each real alternative + the specific tradeoff that lost it. "Do nothing" can
|
|
43
|
+
be a valid alternative. If you list only the winner, you haven't justified it.>
|
|
44
|
+
|
|
45
|
+
## Consequences
|
|
46
|
+
**Positive:** <what gets easier/safer/faster>
|
|
47
|
+
**Negative:** <what gets harder, what we now must maintain, what risk we accept>
|
|
48
|
+
**Neutral / follow-ups:** <new work this creates, things to revisit later>
|
|
49
|
+
```
|
|
50
|
+
Status starts `Proposed` unless the decision is already final-and-shipped (then `Accepted`). Context describes *why a decision is needed*, not the decision. Consequences MUST include negatives — an ADR with only upsides is propaganda, not a record.
|
|
51
|
+
|
|
52
|
+
5. **Maintain the index.** Keep `docs/adr/README.md` (or `index.md`) as a table: `# | Title | Status | Date`, one row per ADR, linking to each file. Append the new row; update the Status cell of any ADR whose status changed. Create the index if it's missing.
|
|
53
|
+
|
|
54
|
+
6. **On a reversal, supersede — never edit history.** To overturn ADR-N: write a *new* ADR-M for the new decision, and in its Context reference ADR-N. Then change ADR-N's status line to `Superseded by ADR-M` (with a link) — do not rewrite ADR-N's body or delete it. Link both ways: new ADR points back to the one it replaces. Accepted ADRs are immutable except for the Status field.
|
|
55
|
+
|
|
56
|
+
## Common Errors
|
|
57
|
+
|
|
58
|
+
- **Editing an accepted ADR's body to "update" a decision.** Accepted ADRs are an append-only log. A change of mind is a new, superseding ADR — never a rewrite of the old one. Only the Status line of an existing ADR may change.
|
|
59
|
+
- **Reusing/skipping a number.** Numbers are permanent references. Deleting ADR-0004 and giving 0004 to a new decision corrupts every link that pointed at the old one. Always take `max + 1`.
|
|
60
|
+
- **No real alternatives section.** Listing only the chosen option (or alternatives with no stated reason for rejection) makes the ADR worthless — the whole value is *why the other paths lost*. Each alternative needs a concrete rejection reason.
|
|
61
|
+
- **Context that already contains the decision.** Context = forces and constraints only. If "we will use X" appears in Context, you've collapsed the record. Keep the problem and the solution in separate sections.
|
|
62
|
+
- **All-positive Consequences.** Every real decision has a cost (new dependency to maintain, lock-in, migration work). Omitting negatives hides the tradeoff future readers most need.
|
|
63
|
+
- **Multiple decisions in one ADR.** "Use Postgres AND adopt hexagonal architecture" is two ADRs. Bundled records can't be individually superseded later. Split them.
|
|
64
|
+
- **Forgetting the index / dangling supersede link.** A new ADR not added to the index, or an old ADR left as `Accepted` after being replaced, makes the log lie about current state. Update both sides.
|
|
65
|
+
- **Leaking private context** — personal/author names, machine paths, internal tickets, secrets in Context or Decision. Keep it about the system, with generic placeholders.
|
|
66
|
+
|
|
67
|
+
## Verify
|
|
68
|
+
|
|
69
|
+
- [ ] Decision stated as one clear "We will ..." sentence; exactly one decision in the record.
|
|
70
|
+
- [ ] File is `docs/adr/NNNN-kebab-title.md` with `NNNN = highest existing + 1`, zero-padded; no number reused.
|
|
71
|
+
- [ ] All five sections present and correctly scoped: Status, Context (forces only, no solution), Decision, Alternatives Considered (each with a why-rejected reason), Consequences (includes negatives).
|
|
72
|
+
- [ ] Status is a valid value (`Proposed`/`Accepted`/`Deprecated`/`Superseded by ADR-MMMM`) and matches reality.
|
|
73
|
+
- [ ] Index (`README.md`/`index.md`) has a row for this ADR linking to it; statuses there match the files.
|
|
74
|
+
- [ ] If this reverses a prior decision: old ADR's Status changed to `Superseded by ADR-N` (body untouched), and the new ADR links back to it — both directions.
|
|
75
|
+
- [ ] No personal identifiers, absolute home paths, internal URLs, or secrets anywhere in the record.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-analytical-sql
|
|
3
|
+
description: Writes accurate, readable, dialect-correct analytical SQL — CTEs, window functions, aggregations, and joins — across Postgres, BigQuery, Snowflake, and Databricks.
|
|
4
|
+
when_to_use: When the user needs an analytical SQL query written or translated — build a report query with CTEs/window functions/aggregations, express business logic in SQL, or translate a query between warehouse dialects (Postgres/BigQuery/Snowflake/Databricks).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use when the task is to **write or translate analytical SQL** — reports, metrics, cohort/funnel logic, dedup, running totals, rankings — in Postgres, BigQuery, Snowflake, or Databricks SQL.
|
|
10
|
+
|
|
11
|
+
- Use for: report queries with CTEs/window functions, expressing business logic in SQL, dialect translation preserving semantics.
|
|
12
|
+
- Do NOT use for: query performance tuning (use optimize-sql-query) or schema migrations (out of scope).
|
|
13
|
+
- Hard rule: **never invent column or table names.** If schema is unknown, inspect or ask before writing — a wrong column name is worse than a question.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
1. **Lock the dialect first.** If unstated, ask or infer from connection/context. Dialect changes date functions, dedup syntax (QUALIFY), array/struct access, and string concat — get it wrong and the query won't parse.
|
|
18
|
+
2. **Get the real schema.** Inspect via `information_schema.columns` / `\d table` / `DESCRIBE table`, or ask. Confirm for every joined table: the **grain** (one row per what?), the join keys, and which columns are nullable. Grain ambiguity is the #1 source of wrong numbers.
|
|
19
|
+
3. **Decompose into named CTEs**, one logical step per CTE (filter → join → aggregate → rank → final select). Name them for intent (`active_users`, `daily_revenue`), not `cte1`. Keep the final `SELECT` thin.
|
|
20
|
+
4. **Pick the right join type and verify grain.** Before any join, confirm the right side is unique on the join key. If it can have multiple rows, you have **fan-out** — pre-aggregate the right side in a CTE first, or the join multiplies your fact rows and inflates every `SUM`/`COUNT`.
|
|
21
|
+
5. **Use window functions instead of self-joins** for ranking, running totals, dedup, period-over-period:
|
|
22
|
+
- Dedup / latest-per-group: `ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)` then keep `= 1`.
|
|
23
|
+
- Running total: `SUM(x) OVER (PARTITION BY ... ORDER BY dt ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`.
|
|
24
|
+
- Period-over-period: `LAG(metric) OVER (ORDER BY period)`.
|
|
25
|
+
- Be explicit about the frame — default frame is `RANGE UNBOUNDED PRECEDING AND CURRENT ROW`, which silently sums ties together; use `ROWS` when you mean row-by-row.
|
|
26
|
+
6. **Guard aggregates against NULL and the empty set.**
|
|
27
|
+
- `COUNT(col)` skips NULLs; `COUNT(*)` doesn't — pick deliberately. Distinct users = `COUNT(DISTINCT user_id)`.
|
|
28
|
+
- `SUM` over zero rows returns NULL, not 0 — wrap with `COALESCE(SUM(x), 0)` when a numeric default is expected.
|
|
29
|
+
- `AVG` ignores NULLs in the denominator; if NULL means zero, `COALESCE` before averaging.
|
|
30
|
+
- Filtered aggregates: `COUNT(*) FILTER (WHERE cond)` (Postgres/Databricks) or `COUNT(CASE WHEN cond THEN 1 END)` (portable, works everywhere).
|
|
31
|
+
7. **Apply dialect specifics** (see table below) for dates, dedup, arrays, and row limiting.
|
|
32
|
+
8. **For translation:** map semantics, not text. Re-express date math, dedup, array/struct access, and `QUALIFY`/`LIMIT`/`TOP` per the target dialect. Re-check that NULL and casting behavior still match the source intent.
|
|
33
|
+
9. **Format for review:** keywords uppercase, leading commas or trailing — pick one, one join per line, CTEs separated by blank lines. Add a one-line comment stating what the query answers and its output grain.
|
|
34
|
+
10. **Validate** on a sample if a DB is reachable (see Verify).
|
|
35
|
+
|
|
36
|
+
### Dialect cheat-sheet
|
|
37
|
+
|
|
38
|
+
| Need | Postgres | BigQuery | Snowflake | Databricks (Spark SQL) |
|
|
39
|
+
|---|---|---|---|---|
|
|
40
|
+
| Current date | `CURRENT_DATE` | `CURRENT_DATE()` | `CURRENT_DATE` | `current_date()` |
|
|
41
|
+
| Truncate to month | `DATE_TRUNC('month', d)` | `DATE_TRUNC(d, MONTH)` | `DATE_TRUNC('month', d)` | `date_trunc('month', d)` |
|
|
42
|
+
| Date diff (days) | `d2 - d1` | `DATE_DIFF(d2, d1, DAY)` | `DATEDIFF(day, d1, d2)` | `datediff(d2, d1)` |
|
|
43
|
+
| Add interval | `d + INTERVAL '7 day'` | `DATE_ADD(d, INTERVAL 7 DAY)` | `DATEADD(day, 7, d)` | `date_add(d, 7)` |
|
|
44
|
+
| Dedup top-1 per group | `ROW_NUMBER()` in CTE, filter outer | `QUALIFY ROW_NUMBER()...=1` | `QUALIFY ROW_NUMBER()...=1` | `QUALIFY ROW_NUMBER()...=1` |
|
|
45
|
+
| Row limit | `LIMIT n` | `LIMIT n` | `LIMIT n` (not `TOP`) | `LIMIT n` |
|
|
46
|
+
| String concat | `\|\|` or `CONCAT` | `CONCAT` (`\|\|` ok) | `\|\|` or `CONCAT` | `\|\|` or `concat` |
|
|
47
|
+
| Safe divide | `x / NULLIF(y,0)` | `SAFE_DIVIDE(x,y)` | `x / NULLIF(y,0)` | `x / NULLIF(y,0)` |
|
|
48
|
+
| Array element | `arr[1]` (1-based) | `arr[OFFSET(0)]` (0-based) | `arr[0]` (0-based) | `arr[0]` (0-based) |
|
|
49
|
+
|
|
50
|
+
## Common Errors
|
|
51
|
+
|
|
52
|
+
- **Postgres has no `QUALIFY`.** Wrap the window function in a CTE/subquery and filter `WHERE rn = 1` in the outer query. BigQuery/Snowflake/Databricks support `QUALIFY` directly.
|
|
53
|
+
- **`GROUP BY` ordinals.** Postgres/Snowflake/Databricks accept `GROUP BY 1, 2`; prefer explicit column names in CTEs so a column reorder doesn't silently regroup.
|
|
54
|
+
- **Fan-out from a one-to-many join silently inflates `SUM`/`COUNT`.** Always pre-aggregate the many-side to the fact's grain before joining. Symptom: totals are too high and don't reconcile.
|
|
55
|
+
- **`COUNT(DISTINCT)` is the fix when fan-out already happened**, but it's a band-aid — fixing the grain is correct. Don't reach for `DISTINCT` to hide a join bug.
|
|
56
|
+
- **`WHERE` on a `LEFT JOIN`ed table turns it into an `INNER JOIN`.** Filters on the right table belong in the `ON` clause, or the unmatched (NULL) rows get dropped.
|
|
57
|
+
- **`NULL` never equals `NULL`.** `x = NULL` is always false — use `IS NULL` / `IS DISTINCT FROM`. `NOT IN (subquery with a NULL)` returns zero rows; use `NOT EXISTS`.
|
|
58
|
+
- **BigQuery arrays are 0-based with `OFFSET`; Postgres is 1-based.** Off-by-one when translating array access is a silent wrong-row bug.
|
|
59
|
+
- **Integer division truncates** in Postgres/Snowflake/Databricks (`5/2 = 2`). Cast one side: `x::numeric / y` or `CAST(x AS FLOAT64)/y` (BigQuery).
|
|
60
|
+
- **Window frame defaults to `RANGE`, which lumps tied `ORDER BY` rows together** in running totals. Use `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` for true row-by-row cumulation.
|
|
61
|
+
- **`HAVING` filters after aggregation, `WHERE` before.** Putting an aggregate condition in `WHERE` errors; putting a row filter in `HAVING` works but scans more than needed.
|
|
62
|
+
- **Timezone drift:** BigQuery `TIMESTAMP` is UTC; casting to `DATE` without a timezone shifts day boundaries. Be explicit: `DATE(ts, 'Asia/Bangkok')`.
|
|
63
|
+
|
|
64
|
+
## Verify
|
|
65
|
+
|
|
66
|
+
- **Parse/compile:** if a DB is reachable, run the query (or `EXPLAIN` it) — confirm it executes and column names resolve.
|
|
67
|
+
- **Grain check:** run `SELECT key, COUNT(*) FROM result GROUP BY key HAVING COUNT(*) > 1` on the claimed unique key; expect zero rows. If not, you have fan-out or a dedup miss.
|
|
68
|
+
- **Reconcile a total:** compute one headline metric two independent ways (e.g. `SUM` of the detail vs. a separate aggregate) and confirm they match — catches join inflation.
|
|
69
|
+
- **NULL/empty edge:** confirm the query returns a sensible value (0 vs NULL) when a group has no rows, per the `COALESCE` decision in Step 6.
|
|
70
|
+
- **Translation parity:** if translating, run source and target against the same sample and diff the result sets — they must be identical, not just "look right."
|
|
71
|
+
- State the **output grain and what the query answers** in one line so the caller can sanity-check intent against numbers.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-data-viz
|
|
3
|
+
description: Produces data visualizations and dashboards as code — matplotlib/seaborn/plotly static and interactive charts, plus D3.js/HTML dashboards — picking chart types that fit the data.
|
|
4
|
+
when_to_use: When the user asks to visualize data or build a chart/dashboard — plot a trend, comparison, distribution, or relationship, generate an interactive Plotly/D3 chart, or assemble a multi-panel dashboard from a dataset.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Use when the request is to turn data into a visual artifact in code: plot a trend/comparison/distribution/relationship, build an interactive Plotly/D3 chart, or assemble a multi-panel dashboard from a dataset (CSV/JSON/DataFrame/SQL result).
|
|
10
|
+
|
|
11
|
+
Not this skill: native Excel/Sheets charts embedded in a workbook (that is the spreadsheet skill — this one emits code and web artifacts, png/svg/html).
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. **Inspect the data first — do not guess.** Load it and check shape before plotting: row count, dtypes, null counts, cardinality of categorical columns, min/max of numerics. In pandas: `df.info()`, `df.describe()`, `df.isna().sum()`, `df[col].nunique()`. Decide here whether aggregation is needed (e.g. >5k points → sample/bin; >12 categories → top-N + "Other").
|
|
16
|
+
|
|
17
|
+
2. **Map analytical goal → chart type** (the goal is in the request verb, not the user's wording):
|
|
18
|
+
- trend over time → line (or area for cumulative)
|
|
19
|
+
- compare categories → bar (horizontal bar if labels are long or >7 categories)
|
|
20
|
+
- distribution of one variable → histogram or box/violin; KDE if smooth shape matters
|
|
21
|
+
- relationship between two numerics → scatter; add trendline/`hexbin` if dense
|
|
22
|
+
- part-of-whole → stacked bar or treemap — **avoid pie for >3 slices**
|
|
23
|
+
- correlation matrix → heatmap
|
|
24
|
+
- ranking → sorted horizontal bar
|
|
25
|
+
When unsure between two, default to the one with less ink (bar over pie, line over stacked area).
|
|
26
|
+
|
|
27
|
+
3. **Pick the engine by output requirement, not by habit:**
|
|
28
|
+
- static report image (png/svg) → `matplotlib` + `seaborn`
|
|
29
|
+
- interactive (hover/zoom/toggle) needed → `plotly` (`plotly.express` for speed, `graph_objects` for control)
|
|
30
|
+
- embeddable/standalone web dashboard, custom interaction, or no-Python-runtime delivery → `D3.js` in a single self-contained HTML file
|
|
31
|
+
Confirm the library is installed (`python -c "import plotly"`) before writing the full script; if missing, install or fall back to matplotlib and say so.
|
|
32
|
+
|
|
33
|
+
4. **Annotate every chart** — non-negotiable, in this order: title (states the takeaway, not "Chart of X"), axis labels **with units**, legend only when >1 series, source/date note if relevant. Format axis ticks for humans (thousands separators, `%`, dates as `%b %Y`). Sort bars by value unless the x-axis is inherently ordered (time, ordinal).
|
|
34
|
+
|
|
35
|
+
5. **Style:** use a colorblind-safe palette (matplotlib `tab10`/`viridis`, seaborn `colorblind`, plotly default Safe). Strip chartjunk: no 3D, no gridlines on bars, no background fill, no redundant legend. One accent color for the series that matters; grey out the rest. Set `dpi=150`+ for static export.
|
|
36
|
+
|
|
37
|
+
6. **Multi-panel dashboard** when ≥3 related metrics: matplotlib `plt.subplots()` / `GridSpec`, plotly `make_subplots`, or D3 a CSS-grid of `<svg>` panels. Share axes where comparable, give each panel its own clear title, keep one consistent palette across panels.
|
|
38
|
+
|
|
39
|
+
7. **Save the artifact and tell the user how to view it.** Static → `fig.savefig("chart.png", dpi=150, bbox_inches="tight")` (use `bbox_inches="tight"` or labels clip). Interactive plotly → `fig.write_html("dashboard.html")`. D3 → one HTML file with the data inlined or fetched. State the absolute path and the open command (e.g. open the `.html` in a browser).
|
|
40
|
+
|
|
41
|
+
## Common Errors
|
|
42
|
+
|
|
43
|
+
- **Overplotting** — thousands of scatter points become a blob. Fix with `alpha=0.3`, `hexbin`/2D-density, or sampling. Do not ship a solid ink cloud.
|
|
44
|
+
- **Misleading dual axes** — two y-axes invite false correlation and can be scaled to tell any story. Prefer two stacked panels sharing the x-axis; only use twin axes when units are genuinely paired and label both clearly.
|
|
45
|
+
- **Categorical read as numeric** — IDs/year-codes/zip codes loaded as int get a continuous color scale or spaced as numbers. Cast to `str`/`category` first.
|
|
46
|
+
- **Truncated/non-zero bar baseline** — bar charts MUST start the value axis at 0 or they exaggerate differences. Line charts may zoom; bars may not.
|
|
47
|
+
- **bbox clipping** — long tick/axis labels get cut off in saved PNGs. Always `bbox_inches="tight"` and rotate or horizontal-bar long labels.
|
|
48
|
+
- **Too many colors / rainbow** — >7 hues are unreadable. Group, use top-N + Other, or a sequential scale.
|
|
49
|
+
- **Time not parsed** — date strings on the x-axis sort lexically (2026-01 after 2026-1). Parse to datetime before plotting.
|
|
50
|
+
- **Plotly HTML huge/blank offline** — default embeds the full lib (~3MB) and a blank page if CDN is blocked; pass `include_plotlyjs="cdn"` for size or `=True` for offline-safe.
|
|
51
|
+
|
|
52
|
+
## Verify
|
|
53
|
+
|
|
54
|
+
1. Script runs to completion with no exception, and the output file exists on disk (check the path).
|
|
55
|
+
2. Open/inspect the rendered artifact — title, both axis labels+units, and legend are present and not clipped.
|
|
56
|
+
3. Chart type matches the analytical goal from Step 2 (a trend is a line, not a bar; bars start at 0).
|
|
57
|
+
4. No overplotting/rainbow/dual-axis trap from Common Errors slipped through.
|
|
58
|
+
5. For interactive output, confirm hover/zoom actually works (open the HTML); for a dashboard, every panel rendered and shares a consistent palette.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-docs
|
|
3
|
+
description: Generates and updates project-facing documentation from the actual codebase — README, API reference, usage examples, and changelog entries — staying in sync with real code, signatures, and config. Use when docs are missing, stale, or after a feature/API change.
|
|
4
|
+
when_to_use: README/docs ขาดหรือ stale; หลังเพิ่ม feature/เปลี่ยน API; ต้องสร้าง changelog/usage example
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
- No README, or README's commands/flags no longer match the code.
|
|
10
|
+
- A feature, CLI command, function signature, or config key was added/renamed/removed.
|
|
11
|
+
- Need a changelog entry for a release, or runnable usage examples.
|
|
12
|
+
|
|
13
|
+
Do NOT use for inline code comments or design docs — this skill produces user-facing docs only.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
1. **Scan real structure first — never write from memory.**
|
|
18
|
+
- Find entrypoint(s): `package.json` `bin`/`main`/`exports`, `pyproject.toml` `[project.scripts]`, `Cargo.toml` `[[bin]]`, or the file the README claims. Confirm it exists.
|
|
19
|
+
- List runnable commands/scripts: `package.json` `scripts`, `Makefile` targets, declared CLI subcommands.
|
|
20
|
+
- Enumerate public API: exported functions/classes/types (what's in `index`/`__init__`/`lib.rs`/public exports), not internal helpers.
|
|
21
|
+
- Read config sources: env vars actually referenced in code (`grep` for `process.env.`, `os.environ`, `Deno.env`), config-file schema, default values. Use the real defaults from code, not guesses.
|
|
22
|
+
|
|
23
|
+
2. **Generate/update README** with sections, in this order:
|
|
24
|
+
- **Install** — exact command from the real package manager + manifest (`npm i <name>`, `pip install <name>`, etc.). Verify the package name from the manifest, don't assume the repo name.
|
|
25
|
+
- **Quickstart** — smallest runnable example that produces visible output.
|
|
26
|
+
- **Usage** — each command/flag with real names and real defaults.
|
|
27
|
+
- **Config** — a table: `Key | Type | Default | Description`, populated from code-confirmed values only.
|
|
28
|
+
- Preserve existing hand-written prose; replace only the stale/code-derived parts. Don't nuke sections you can't regenerate.
|
|
29
|
+
|
|
30
|
+
3. **Write API reference from real signatures.** For each public symbol, pull the actual signature (params, types, return, throws) from source/type definitions. Match parameter names and order exactly. If a param has a default in code, show that default. Omit private/underscored/unexported symbols.
|
|
31
|
+
|
|
32
|
+
4. **Build changelog from commits.** `git log <last-tag>..HEAD --oneline`. Parse Conventional Commits (`feat:`, `fix:`, `perf:`, `refactor:`, `docs:`, `chore:`...). Group under headings: **Added** (feat), **Fixed** (fix), **Changed** (refactor/perf), etc. Mark `BREAKING CHANGE:`/`!` commits prominently. Drop noise (merge commits, `chore: bump`, CI). Prepend a new version section above existing entries — never rewrite history.
|
|
33
|
+
|
|
34
|
+
5. **Verify every example runs.** Execute each quickstart/usage snippet (or `--help` for CLI flags) and confirm it succeeds. If a snippet can't be run in this environment, mark it clearly and base it strictly on a verified signature, not invention.
|
|
35
|
+
|
|
36
|
+
## Common Errors
|
|
37
|
+
|
|
38
|
+
- **Doc drifts from code** — writing a flag/param/default from the old README or assumption instead of re-reading source. Always re-scan; the README is a suspect, not a source of truth.
|
|
39
|
+
- **Wrong install name** — using the repo/folder name instead of the published package name in the manifest.
|
|
40
|
+
- **Hallucinated options** — listing flags/config keys that aren't referenced anywhere in code. If `grep` doesn't find it, it doesn't go in the docs.
|
|
41
|
+
- **Examples that error** — copy-paste snippets with stale imports, removed args, or wrong call order. Run them.
|
|
42
|
+
- **Leaking private data** — absolute home paths, machine names, personal/author identifiers, tokens, internal URLs in examples. Use generic placeholders (`/path/to/project`, `<your-api-key>`, `example.com`).
|
|
43
|
+
- **AI-look output** — emoji-spammed headers, rainbow callouts, padded marketing prose. Keep it plain, technical, scannable. No emoji unless the project already uses them.
|
|
44
|
+
- **Destroying prose** — overwriting hand-authored explanation/architecture notes while regenerating a code-derived section. Edit surgically.
|
|
45
|
+
|
|
46
|
+
## Verify
|
|
47
|
+
|
|
48
|
+
- Every command, flag, and config key in the docs maps to a real occurrence in the code (spot-check with `grep`).
|
|
49
|
+
- Install command uses the actual package name from the manifest.
|
|
50
|
+
- Each quickstart/usage example was executed and exited successfully (or is explicitly marked unrunnable + signature-verified).
|
|
51
|
+
- API signatures match source exactly (names, order, defaults, return type).
|
|
52
|
+
- Changelog groups commits by Conventional-Commit type, flags breaking changes, and adds a new section without altering past entries.
|
|
53
|
+
- No personal paths, identifiers, or secrets; no emoji/AI-look styling.
|
|
54
|
+
- Existing hand-written sections preserved.
|