first commit
Claude-Session: https://claude.ai/code/session_014PM3SXLZ3PUivkMQ8QXTxT
This commit is contained in:
commit
b155475c8c
3 changed files with 488 additions and 0 deletions
169
kvm-zvol-restore/SKILL.md
Normal file
169
kvm-zvol-restore/SKILL.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
---
|
||||||
|
name: kvm-zvol-restore
|
||||||
|
description: Restore KVM/libvirt VM disks from borg backups onto ZFS zvols exported over iSCSI. Use when asked to restore, roll back, or recover a VM's drive to a previous date, or to inspect what VM disk backups are available. Covers the tama/kiki zvol+iSCSI setup backed up by kvm-backup-zvol-iscsi.sh.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Restoring KVM VM disks from borg to ZFS zvols
|
||||||
|
|
||||||
|
## Topology
|
||||||
|
|
||||||
|
Two hosts, and it matters which one you run each command on:
|
||||||
|
|
||||||
|
| Host | Name | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `10.0.0.5` | `kiki` | libvirt host (runs the VMs) **and** holds the borg repo at `/media/Storage/Backup/kvm` |
|
||||||
|
| `10.0.0.6` | `tama` | ZFS server: owns the zvols (`tank/kvm/*`), exports them via iSCSI (LIO/targetcli), runs the backup script |
|
||||||
|
|
||||||
|
The backup script is `/usr/local/bin/kvm-backup-zvol-iscsi.sh` **on tama (.6)**, not on kiki.
|
||||||
|
It discovers domains via `virsh` locally and on `REMOTE_HOSTS` (`kiki`, `gaming-pc`), maps each
|
||||||
|
iSCSI-backed disk to a local zvol, snapshots it, and `dd`s the snapshot device into borg.
|
||||||
|
|
||||||
|
VMs reach their disks over the **storage network** `10.0.100.6:3260`, not `10.0.0.6`.
|
||||||
|
|
||||||
|
## What the backups actually are
|
||||||
|
|
||||||
|
Raw whole-device images (`dd` of a ZFS snapshot block device) piped into borg as **stdin**, so each
|
||||||
|
archive holds exactly one item named `stdin`. There is no filesystem structure inside — restoring
|
||||||
|
means writing the stream back over the whole block device.
|
||||||
|
|
||||||
|
Archive naming:
|
||||||
|
|
||||||
|
```
|
||||||
|
<domain>-<target-dev>-<YYYY-MM-DDTHH:MM:SS> disk image, e.g. centos7-vda-2026-08-14T07:38:41
|
||||||
|
<domain>-xml-<YYYY-MM-DDTHH:MM:SS> virsh dumpxml output
|
||||||
|
```
|
||||||
|
|
||||||
|
Retention is `--keep-daily 7 --keep-weekly 4 --keep-monthly 6`, so anything older than a week
|
||||||
|
only exists at weekly/monthly granularity. Check before promising a specific date.
|
||||||
|
|
||||||
|
## Gotchas that have actually bitten
|
||||||
|
|
||||||
|
1. **Archive names contain colons.** `centos7-vda-2026-08-14T07:38:41`. Never use `:` as a field
|
||||||
|
separator when building job lists in a shell script — it silently truncates the name to
|
||||||
|
`centos7-vda-2026-08-14T07` and borg reports "Archive does not exist". Use `|`.
|
||||||
|
2. **`borg list --format "{archive}"` fails on an archive.** `{archive}` is a repo-level key; using
|
||||||
|
it while listing archive *contents* errors out and looks exactly like a missing archive.
|
||||||
|
To test existence use `borg info "::$ARCHIVE"`.
|
||||||
|
3. **The device name is not always `vda`.** `centos6` is `sdb`; `centos7`/`rocky9` are `vda`.
|
||||||
|
Always read the device from the archive name or `virsh domblklist`.
|
||||||
|
4. **`virsh domblklist` shows CDROMs with source `-`.** Skip those; only `-iscsi-` paths are backed up.
|
||||||
|
5. **Domain name vs zvol name is case-insensitive.** IQN target names are lowercased
|
||||||
|
(`iqn...tama:centos7-2`) while the zvol may be `tank/kvm/CentOS7-2`. There are also stale archives
|
||||||
|
from an older capitalized `Centos7` domain — don't confuse them with current `centos7`.
|
||||||
|
6. **CentOS 6 ignores `virsh shutdown`** (no/old acpid). It may need several minutes or a
|
||||||
|
`virsh destroy`. Never start writing until `virsh domstate` says `shut off`.
|
||||||
|
7. **Don't run during the backup window** (~07:24–07:55 daily). The backup holds
|
||||||
|
`/tmp/backup-zvol-iscsi.pid` on tama and would be snapshotting the same zvols.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Shut down the VMs (on kiki, .5)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.5 'for d in vm1 vm2; do virsh shutdown "$d"; done'
|
||||||
|
# then poll until every one reports "shut off" — do not proceed otherwise
|
||||||
|
ssh root@10.0.0.5 'for i in $(seq 1 30); do s=$(virsh domstate vm1); [ "$s" = "shut off" ] && break; sleep 5; done; virsh domstate vm1'
|
||||||
|
```
|
||||||
|
|
||||||
|
If a guest won't go down gracefully, confirm with the user before `virsh destroy`.
|
||||||
|
|
||||||
|
### 2. Find the archives (from tama, .6)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.6 'export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
|
||||||
|
borg list --format "{archive}{TAB}{time}{NL}" root@10.0.0.5:/media/Storage/Backup/kvm' \
|
||||||
|
| grep -Ei "^(vm1|vm2)-"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then record each archive's exact byte size — it must equal the zvol's size:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borg info "::$ARCHIVE" # "Original size" column, or borg list shows the stdin item size
|
||||||
|
blockdev --getsize64 /dev/zvol/tank/kvm/<zvol>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Map disks to zvols
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.5 'virsh domblklist <domain>' # dev -> /dev/disk/by-path/...-iscsi-iqn...:<target>-lun-0
|
||||||
|
ssh root@10.0.0.6 'zfs list -o name,volsize,volmode -r tank/kvm -d 1'
|
||||||
|
```
|
||||||
|
The zvol is the IQN target name after the last colon, matched case-insensitively under `tank/kvm/`.
|
||||||
|
|
||||||
|
### 4. Optional rollback point
|
||||||
|
|
||||||
|
A pre-restore `zfs snapshot tank/kvm/<z>@pre-restore-<date>` is cheap insurance, **but ask first** —
|
||||||
|
it is not always wanted, and the daily backup snapshots (`SNAPSHOTS_KEEP=2`) may already cover it.
|
||||||
|
|
||||||
|
### 5. Restore (on tama, .6)
|
||||||
|
|
||||||
|
Run detached — 500 GiB takes ~20 min at ~475 MB/s, and an SSH drop must not kill it.
|
||||||
|
Guard every device write behind a size check and an existence check.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
export BORG_REPO="root@10.0.0.5:/media/Storage/Backup/kvm"
|
||||||
|
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
|
||||||
|
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# zvol|archive|expected_bytes (pipe-delimited: archive names contain colons)
|
||||||
|
JOBS=(
|
||||||
|
"centos7|centos7-vda-2026-08-14T07:38:41|536870912000"
|
||||||
|
)
|
||||||
|
|
||||||
|
for J in "${JOBS[@]}"; do
|
||||||
|
IFS="|" read -r ZVOL ARCHIVE EXPECT <<<"$J"
|
||||||
|
DEV="/dev/zvol/tank/kvm/$ZVOL"
|
||||||
|
echo "=== $(date "+%F %T") restoring $ARCHIVE -> $DEV ($EXPECT bytes)"
|
||||||
|
|
||||||
|
[[ -b "$DEV" ]] || { echo "FAIL: $DEV not a block device"; exit 1; }
|
||||||
|
|
||||||
|
ACTUAL=$(blockdev --getsize64 "$DEV")
|
||||||
|
[[ "$ACTUAL" == "$EXPECT" ]] || { echo "FAIL: $DEV is $ACTUAL, archive is $EXPECT"; exit 1; }
|
||||||
|
|
||||||
|
borg info "::$ARCHIVE" >/dev/null 2>&1 || { echo "FAIL: archive $ARCHIVE not found"; exit 1; }
|
||||||
|
|
||||||
|
borg extract --stdout "::$ARCHIVE" \
|
||||||
|
| dd of="$DEV" bs=4M iflag=fullblock conv=fsync status=progress \
|
||||||
|
|| { echo "FAIL: restore of $ARCHIVE failed"; exit 1; }
|
||||||
|
|
||||||
|
echo "=== $(date "+%F %T") completed $ZVOL"
|
||||||
|
done
|
||||||
|
echo "=== $(date "+%F %T") ALL RESTORES COMPLETE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch and watch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.6 'nohup setsid /root/restore.sh > /root/restore.log 2>&1 </dev/null & echo $!'
|
||||||
|
ssh root@10.0.0.6 'grep -aE "^===|FAIL:" /root/restore.log; tail -c 120 /root/restore.log'
|
||||||
|
```
|
||||||
|
|
||||||
|
`iflag=fullblock` matters: without it `dd` can do short reads from the pipe and write a short image.
|
||||||
|
Writing to `/dev/zvol/...` while LIO exports it is safe **only** because the guest is powered off —
|
||||||
|
LIO takes no exclusive lock, so a running VM would race and corrupt.
|
||||||
|
|
||||||
|
### 6. Flush stale initiator cache (on kiki, .5)
|
||||||
|
|
||||||
|
The zvol changed underneath the iSCSI initiator, so kiki's block-layer cache for that LUN is stale.
|
||||||
|
Before booting, flush it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.5 'blockdev --flushbufs /dev/disk/by-path/ip-10.0.100.6:3260-iscsi-iqn.2026-03.im.gec.tama:<target>-lun-0'
|
||||||
|
```
|
||||||
|
|
||||||
|
A `iscsiadm -m node -T <iqn> --rescan`, or logout/login of the session, is the heavier alternative.
|
||||||
|
|
||||||
|
### 7. Boot and verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@10.0.0.5 'virsh start <domain>; virsh domstate <domain>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Restoring the domain XML is a **separate** decision — the `-xml-` archives exist, but rolling back a
|
||||||
|
disk rarely needs the definition rolled back too, and doing so can undo unrelated config. Ask first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borg extract --stdout "::<domain>-xml-<ts>" > /tmp/<domain>.xml # then virsh define
|
||||||
|
```
|
||||||
165
unit-testing/SKILL.md
Normal file
165
unit-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
---
|
||||||
|
name: unit-testing
|
||||||
|
description: My unit-testing style. Use whenever writing, reviewing, or pruning unit tests in any project, before writing the first test function. Covers what deserves a unit test, what must not be tested, and the required mechanics (testify, table-driven, real dependencies first, mocks only as a last resort).
|
||||||
|
---
|
||||||
|
|
||||||
|
# Unit testing style
|
||||||
|
|
||||||
|
I want few, high-value tests, in two tiers.
|
||||||
|
|
||||||
|
1. **Unit tests.** Small, fast, and mainly covering serialization boundaries:
|
||||||
|
marshal/unmarshal, parse/encode, round-trips, and on-disk or wire-format
|
||||||
|
invariants.
|
||||||
|
2. **Integration tests.** The primary safety net. They exercise the project as
|
||||||
|
a whole against real systems (real backends, real servers, real databases)
|
||||||
|
and usually live in a separate suite behind build tags, run with
|
||||||
|
`make test-integration`.
|
||||||
|
|
||||||
|
More tests are not better. I have had redundant tests removed by the dozens
|
||||||
|
("Slim down the tests", "remove simple logic tests"). Write fewer tests than
|
||||||
|
feels natural, and never pad coverage.
|
||||||
|
|
||||||
|
## What deserves a unit test
|
||||||
|
|
||||||
|
- **Serialization and parsing.** JSON/YAML marshal and unmarshal round-trips,
|
||||||
|
config parsing, enum-to-string stability (a backup must stay readable even
|
||||||
|
if an iota constant gets reordered). For codecs, encode, decode, and
|
||||||
|
compare, with the invalid-input case paired in the same test.
|
||||||
|
- **External-format invariants.** Scan/rewrite behavior against fixtures
|
||||||
|
shaped exactly like what the external tool or system really emits,
|
||||||
|
especially the shapes an integration run can't easily seed.
|
||||||
|
- **Contract tests against an external system's declared interface.** Assert
|
||||||
|
the encoder's output against what the external system's spec or
|
||||||
|
introspection declares; the declared signature or schema string can itself
|
||||||
|
be the assertion. Include tolerance tests: truncated or old-version input
|
||||||
|
must not panic or misalign.
|
||||||
|
- **Facts about the outside world** the code depends on: a real tool's exact
|
||||||
|
output spelling, a protocol quirk, a version-capability boundary pinned to
|
||||||
|
the real deployment targets. These stay even when the test is four lines.
|
||||||
|
- **Real algorithmic logic** with a non-obvious result: coverage and overlap
|
||||||
|
relations, canonicalization that guards against churn, index math, scoring.
|
||||||
|
One representative test per axis, never an exhaustive fan-out.
|
||||||
|
- **API-boundary behavior** of a service. Drive the real handler with
|
||||||
|
`httptest` and assert observable effects (status codes, files written or
|
||||||
|
removed), not internals.
|
||||||
|
|
||||||
|
## What must NOT be unit tested
|
||||||
|
|
||||||
|
- **Simple logic.** Trivial predicates, single-field identity assertions
|
||||||
|
("Equal ignores field X"), tests that restate a small helper's own body.
|
||||||
|
These are useless. They don't test anything worthwhile.
|
||||||
|
- **Anything the integration suite already proves live.** Lifecycle
|
||||||
|
operations (add, remove, sync, backup, restore) that a named integration
|
||||||
|
subtest pushes through a real backend don't get duplicated in miniature
|
||||||
|
with fakes.
|
||||||
|
- **Pure reject-only guards**, meaning a lone `require.Error` on one
|
||||||
|
validation. The real system fails loudly anyway. A rejection test earns its
|
||||||
|
place only when it also pins a positive boundary case next to the
|
||||||
|
rejection.
|
||||||
|
- **Mock-based tests of internal collaborators.** Mock-manager style tests
|
||||||
|
get deleted. Mocks aren't banned outright, see "Real dependencies" below,
|
||||||
|
but mocking your own internals never qualifies.
|
||||||
|
|
||||||
|
For borderline cases, one question decides: does the test encode an external
|
||||||
|
fact? If it pins a real spelling, format, or quirk of a system outside this
|
||||||
|
codebase, keep it. If it only restates this codebase's own code, delete it. A
|
||||||
|
bug there fails loudly downstream.
|
||||||
|
|
||||||
|
These rules govern what gets committed, not how to work. A throwaway test to
|
||||||
|
confirm behavior while developing or debugging is fine, even for simple
|
||||||
|
logic. Write it, run it, learn from it, then delete it before the work is
|
||||||
|
done. It must not land in the suite.
|
||||||
|
|
||||||
|
## Mechanics
|
||||||
|
|
||||||
|
- Go tests use `testing` plus `github.com/stretchr/testify`, never
|
||||||
|
hand-rolled `if got != want`. Some older projects predate this, but every
|
||||||
|
migration has been toward testify. Split the roles: `require` for setup and
|
||||||
|
anything that invalidates the rest of the test, `assert` for the actual
|
||||||
|
claims, so one run reports every wrong field.
|
||||||
|
- Assertion messages are sentences that carry the reason, not labels:
|
||||||
|
`assert.Equal(t, 1, *calls, "a ready service must not be polled twice")`.
|
||||||
|
In tables, the message identifies the case: `"signature mismatch for %s"`.
|
||||||
|
- Go table-driven wherever more than one input shape exists: anonymous struct
|
||||||
|
slice with `name`, inputs, and `want` fields. Rows may carry a `why` column
|
||||||
|
so the table holds the rationale ("leading zeros do not change the value").
|
||||||
|
`t.Run(tc.name, ...)` is optional when the assert message already names the
|
||||||
|
case. Sub-test names outside tables read as full English claims:
|
||||||
|
`t.Run("metric zero falls back to default", ...)`.
|
||||||
|
- Every test gets a doc comment stating what invariant it locks and why,
|
||||||
|
ideally naming the real-world source: the distro or tool version, the
|
||||||
|
kernel flag, or the regression it pins. Spell out arithmetic inline so an
|
||||||
|
expected number isn't magic; the comment shows the terms that add up to it.
|
||||||
|
- Test helpers are small, local to the file, `t.Helper()`-marked, use
|
||||||
|
`t.Cleanup` to restore anything they change, and build realistic fixtures
|
||||||
|
laid out exactly as the real system writes them.
|
||||||
|
- Re-read from the source of truth before asserting. Re-construct the backend
|
||||||
|
from disk after a write, so the test exercises the written config rather
|
||||||
|
than in-memory state. Re-query the kernel rather than trusting a cached
|
||||||
|
handle.
|
||||||
|
|
||||||
|
## Real dependencies, never substitutes
|
||||||
|
|
||||||
|
- HTTP: `httptest` servers, or a real TLS listener with self-signed test
|
||||||
|
certs when the TLS path itself matters. Never live external services. Live
|
||||||
|
URLs get replaced with a local server plus fixture repos built with docker
|
||||||
|
dummy packages.
|
||||||
|
- Protocol peers: a real in-process server speaking the real wire protocol at
|
||||||
|
the boundary, fed fixture files authored in the protocol's native format,
|
||||||
|
or a fake that decodes real requests and streams captured responses. Drive
|
||||||
|
error paths by mutating the fake mid-test, walking happy, data-missing, and
|
||||||
|
unreachable phases with one golden file per phase.
|
||||||
|
- External commands: fake them as executable scripts on disk, selected via
|
||||||
|
config paths or a prepended `$PATH` entry. The exec boundary stays real.
|
||||||
|
- Databases: the real database engine in a container (testcontainers), never
|
||||||
|
a lighter engine standing in for the one production runs.
|
||||||
|
- Filesystem: real files in `t.TempDir()`, byte-for-byte like production.
|
||||||
|
Functions that walk absolute paths take a root-prefix parameter so a
|
||||||
|
tempdir can stand in for `/`.
|
||||||
|
- System and network APIs: a network namespace (with `runtime.LockOSThread()`,
|
||||||
|
dummy links, and `t.Skip` when not root) lets tests drive the real kernel
|
||||||
|
safely. A fresh namespace per test is the isolation.
|
||||||
|
- OS backends beyond that belong to the integration suite, which may be a
|
||||||
|
cross-compiled test binary shipped to a target host. Destructive
|
||||||
|
integration tests operate only on throwaway PID-named objects, never a
|
||||||
|
system default or live resource whose loss could cut the SSH session.
|
||||||
|
Cleanup uses `context.Background()` rather than the
|
||||||
|
possibly-cancelled test context, and pre-deletes idempotently.
|
||||||
|
|
||||||
|
Fixtures: prefer real captured output, checked into `testdata/` with
|
||||||
|
provenance noted (which host, which tool version). Assert on specific parsed
|
||||||
|
fields with a comment per quirk, rather than golden-diffing everything.
|
||||||
|
Synthetic samples are a fallback for conditions no real capture shows, and
|
||||||
|
get labeled synthetic. Whole-output golden files (metrics exposition,
|
||||||
|
rendered configs) are compared with a diff that prints the golden path on
|
||||||
|
mismatch. When two tools report the same underlying state, cross-check
|
||||||
|
their fixtures against each other.
|
||||||
|
|
||||||
|
Mocks are okay only when there is no safe way to exercise the real thing,
|
||||||
|
when no isolation mechanism (namespace, container, tempdir, httptest,
|
||||||
|
in-process protocol server) makes the real dependency safe or practical. The
|
||||||
|
order is real thing, then real thing isolated, then mock as last resort.
|
||||||
|
Never mock to dodge the setup cost of an isolation mechanism that exists.
|
||||||
|
When a test double is warranted, hand-write it at the external boundary: a
|
||||||
|
call-recording fake of the external interface, a plain function field, a
|
||||||
|
closure with a call counter. No gomock, no mockery, no testify-mock, and no
|
||||||
|
interfaces introduced solely for testing. Small documented seams in
|
||||||
|
production code are fine instead: a sentinel test value, a package-level
|
||||||
|
poll-interval knob swapped and restored via `t.Cleanup`, an override
|
||||||
|
timestamp field for deterministic output.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
- Wire testing through the Makefile (`make test`, `make test-integration`)
|
||||||
|
and document that in the README. Verify tooling (schema and config
|
||||||
|
validation) gets its own command and runs under `make test`.
|
||||||
|
- Integration suites sit behind build tags (`//go:build <proj>_integration`)
|
||||||
|
so plain `go test ./...` stays dependency-free. Capability differences are
|
||||||
|
handled with `t.Skip` in both directions: an old server skips the new
|
||||||
|
feature, a new server skips the legacy guard.
|
||||||
|
- After adding or removing tests, `gofmt`, `go vet` (including other-GOOS
|
||||||
|
vets for platform-gated files), and the make target must pass. Confirm
|
||||||
|
removed tests left no dead helpers or unused imports.
|
||||||
|
- When asked to add tests, propose the small set that fits the rules above.
|
||||||
|
When touching an existing suite, look for tests that violate these rules
|
||||||
|
and offer to prune them.
|
||||||
154
unslop/SKILL.md
Normal file
154
unslop/SKILL.md
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
---
|
||||||
|
name: unslop
|
||||||
|
description: Cut AI tells from anything written in my projects. Apply to all prose I ship, including code comments, godoc, error strings, log lines, Svelte UI copy, docs, READMEs, and commit messages. Always apply.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Unslop
|
||||||
|
|
||||||
|
Edit text so it reads like a person who works on this codebase wrote it. This
|
||||||
|
covers every writing surface in a Go/Svelte project: godoc and code comments,
|
||||||
|
error and log messages, UI copy, markdown docs, commit messages, and PR
|
||||||
|
descriptions.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Scan for the patterns below.
|
||||||
|
2. Rewrite. Preserve meaning, match the surrounding voice.
|
||||||
|
3. Self-audit: "What makes this obviously AI generated?" Fix remaining tells.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
|
||||||
|
- Have opinions. State what happened and what it means, don't neutrally list.
|
||||||
|
- Vary rhythm. Short sentences. Then longer ones that take their time.
|
||||||
|
- Be specific. Not "this can cause issues" but "the client stays disconnected
|
||||||
|
and the request times out after 2m".
|
||||||
|
- If a sentence could appear unchanged in another project's docs, it says
|
||||||
|
nothing about this one. Cut it.
|
||||||
|
- Match the existing voice. When two docs cover sibling features, they should
|
||||||
|
read like the same person wrote both: same structure, same phrasing for the
|
||||||
|
same concepts, same level of detail.
|
||||||
|
|
||||||
|
## Go code comments
|
||||||
|
|
||||||
|
- Every commented element (func, struct, var) gets a comment whose first
|
||||||
|
word is the element's name, written as a complete sentence ending in a
|
||||||
|
period. `// ParseSize converts a human-readable size ("2.5G") to bytes.`
|
||||||
|
- A comment states a constraint the code can't show, a real-world fact, or
|
||||||
|
the regression it guards. Never a narration of the next line, where the
|
||||||
|
code came from, or why the change is correct. That last kind is
|
||||||
|
review-speak and turns into noise the moment the change merges.
|
||||||
|
- The exception to the no-narration rule: a function with multiple logical
|
||||||
|
steps uses short section comments to label each phase. "Validate input.",
|
||||||
|
"Persist to database.", "Build response." Those segment the function;
|
||||||
|
they don't narrate individual lines.
|
||||||
|
- Operational tone, written for someone maintaining or debugging the
|
||||||
|
system. No conversational language, jokes, or speculative notes.
|
||||||
|
- Name the external source when there is one: the distro, the tool version,
|
||||||
|
the kernel behavior. Naming the exact distro quirk the branch exists for
|
||||||
|
beats "handle legacy services".
|
||||||
|
- Spell out arithmetic so numbers aren't magic:
|
||||||
|
`// reallocated 8 -> 40+40 capped 80; total 89 -> REPLACE_SOON.`
|
||||||
|
- Match the file's comment density. A file with sparse comments doesn't get
|
||||||
|
a narrated paragraph bolted onto one function.
|
||||||
|
|
||||||
|
## Errors and logs
|
||||||
|
|
||||||
|
- Error strings: lowercase, no trailing punctuation, no "Error:" prefix.
|
||||||
|
Chain context with colons or `%w`: `syncing repo: connecting to source:
|
||||||
|
connection refused`. Say what was being attempted, not "failed to failed
|
||||||
|
to".
|
||||||
|
- An error the operator will read should point at the fix when one is known:
|
||||||
|
"set license.key in the config, or pass --license-key".
|
||||||
|
- Log messages: terse, lowercase, structured fields for the variables, no
|
||||||
|
exclamation marks, no emoji. `task executed` with fields beats "Task was
|
||||||
|
executed successfully!".
|
||||||
|
- Never log or print chatbot phrases: "Successfully completed!", "Oops!",
|
||||||
|
"Something went wrong :(".
|
||||||
|
|
||||||
|
## Svelte and UI copy
|
||||||
|
|
||||||
|
- Sentence case everywhere: buttons, labels, headings, menu items. "Import
|
||||||
|
test", not "Import Test".
|
||||||
|
- Error states name what failed and what to do next. Not "Oops! Something
|
||||||
|
went wrong" but "the server is unreachable; check that the service is
|
||||||
|
running".
|
||||||
|
- Empty states say what will appear there or how to add the first item. No
|
||||||
|
cutesy filler, no illustrations-with-quips tone.
|
||||||
|
- No exclamation marks in UI text. No emoji in buttons, headings, toasts, or
|
||||||
|
placeholder text.
|
||||||
|
- Labels are literal. A field for a slug says "Slug", not "Your unique
|
||||||
|
identifier". Tooltips explain the consequence of the setting, not restate
|
||||||
|
its name.
|
||||||
|
- No marketing adjectives in product UI: "powerful", "seamless", "blazing
|
||||||
|
fast". The interface should describe, not sell.
|
||||||
|
|
||||||
|
## Docs and READMEs
|
||||||
|
|
||||||
|
- Say the thing directly. If a sentence can be replaced by a plainer one
|
||||||
|
that says the same thing, replace it.
|
||||||
|
- A doc stands alone for its task. Don't make the reader open a second doc
|
||||||
|
to use the first one; inline the few lines they need.
|
||||||
|
- Don't restate. If an example already appeared in an earlier section, later
|
||||||
|
sections reference it, they don't duplicate it.
|
||||||
|
- Don't explain external systems. A doc about configuring a third-party
|
||||||
|
integration covers how to configure it here and why, not how the third
|
||||||
|
party's API works.
|
||||||
|
- Structure follows the sibling doc. Same heading shapes, same table format,
|
||||||
|
same key: value conventions, so the docs read as one set.
|
||||||
|
- README covers how to build and test via the Makefile, actual usage, and
|
||||||
|
real configuration. No badge walls, no "Features" bullet lists of
|
||||||
|
adjectives, no roadmap promises.
|
||||||
|
- Headings in sentence case. No decorative emoji. No bold on every proper
|
||||||
|
noun.
|
||||||
|
|
||||||
|
## Commits and PRs
|
||||||
|
|
||||||
|
- Imperative, plain, specific: "Switch unit tests to testify", "Replace
|
||||||
|
polling with retry backoff". No "This PR introduces...", no "Enhanced",
|
||||||
|
no scope theater.
|
||||||
|
- The body states what changed and why, names the bug or behavior pinned,
|
||||||
|
and stops. No summary tables of the diff, no "Testing" section that just
|
||||||
|
says tests pass.
|
||||||
|
|
||||||
|
## Language tells
|
||||||
|
|
||||||
|
- AI vocabulary: additionally, crucial, delve, enhance, fostering, garner,
|
||||||
|
intricate, landscape (abstract), pivotal, robust, seamless, showcase,
|
||||||
|
streamline, tapestry, testament, underscore, utilize, leverage. Use the
|
||||||
|
plain word.
|
||||||
|
- Fancy ways to say "is": "serves as", "stands as", "boasts", "features".
|
||||||
|
Say "is" or "has".
|
||||||
|
- "Not just X, but Y." State the point directly.
|
||||||
|
- Rule of three. Don't force ideas into groups of three; use the natural
|
||||||
|
number.
|
||||||
|
- Filler: "in order to" becomes "to", "due to the fact that" becomes
|
||||||
|
"because", "it is important to note that" gets deleted.
|
||||||
|
- Hedging stacks: "could potentially possibly" becomes "may".
|
||||||
|
- Abstract metaphor nouns: substrate, wedge, vector, nexus, primitive (as
|
||||||
|
noun), harness (as metaphor), surface (as in "API surface"), scaffolding
|
||||||
|
(as metaphor), paradigm, north star, flywheel. Pick the concrete word.
|
||||||
|
- Say what it does, not how it feels. "SQL you can read" names a feeling;
|
||||||
|
"`.toSQL()` returns the exact string sent to the database" names a
|
||||||
|
mechanism. If a sentence can't be restated as a fact, instruction, or
|
||||||
|
number, cut it.
|
||||||
|
- Active voice. "The compiler validates queries", not "queries are
|
||||||
|
validated". Passive only when the actor is unknown or doesn't matter.
|
||||||
|
|
||||||
|
## Punctuation and style tells
|
||||||
|
|
||||||
|
- No em dashes, anywhere. Use periods or commas. Don't swap in parentheses
|
||||||
|
or en dashes as a substitute; end the sentence instead.
|
||||||
|
- Colons before a list or example only, not as mid-sentence connectors.
|
||||||
|
- No arrow chains in prose ("A → B → fails"). Write the sentence. Arrows
|
||||||
|
are fine inside code and code comments where they're the local idiom.
|
||||||
|
- Straight quotes, not curly.
|
||||||
|
- Bold sparingly. A bold lead-in that names an item and ends in a period is
|
||||||
|
fine; a bold label with a colon that restates the line is a tell.
|
||||||
|
- No decorative emoji in headings, bullets, commits, or code.
|
||||||
|
|
||||||
|
## Chat artifacts
|
||||||
|
|
||||||
|
Never let these into committed text: "I hope this helps!", "Let me know
|
||||||
|
if...", "Great question!", "You're absolutely right!", "Found the smoking
|
||||||
|
gun!", cutoff disclaimers like "while specific details are limited". Respond
|
||||||
|
directly, write directly.
|
||||||
Loading…
Add table
Reference in a new issue