chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md

This commit is contained in:
2026-09-10 09:18:56 +00:00
parent 4ee3382e2b
commit 80249350cc
6 changed files with 29 additions and 25 deletions
+4 -4
View File
@@ -4,8 +4,8 @@ on:
push:
# A bare v* now this is its own repository. The old prefixed
# vantagectl/v* tag existed to namespace one component's releases
# inside a shared repository, and unlike the agent's agent/v* which
# the control plane greps release tag names for nothing reads this
# inside a shared repository, and unlike the agent's agent/v* - which
# the control plane greps release tag names for - nothing reads this
# one programmatically. Releases before the move keep their prefixed
# tags in the monorepo's release list.
tags:
@@ -80,7 +80,7 @@ jobs:
# ran; ":latest, rebuilt whenever main moved" cannot be named after the
# fact. It is a separate job from the binaries because it needs a
# docker-capable runner rather than a Go one, and it does not need the
# binaries the image builds from source in its own stage.
# binaries - the image builds from source in its own stage.
image:
runs-on: ubuntu-docker
container: docker:dind
@@ -102,7 +102,7 @@ jobs:
echo "IMAGE_TAG=${VERSION#v}" >> $GITHUB_OUTPUT
# The image build resolves the private vantage-shared module, so
# it needs a credential of its own this job does not share the
# it needs a credential of its own - this job does not share the
# build job's filesystem.
- name: Write the module fetch credential
run: |
+19 -15
View File
@@ -4,7 +4,7 @@ The backup and restore CLI for a Vantage control plane. Extracted from the
`vantage` monorepo with its history; the command is the repository root, so the
module is `gitea.hostxtra.co.uk/vantage/vantage-ctl`.
**The command is still `vantagectl`.** Only the repository is `vantage-ctl`
**The command is still `vantagectl`.** Only the repository is `vantage-ctl` -
the binary, the cobra `Use:` string and every runbook keep the name an operator
actually types.
@@ -30,7 +30,7 @@ failure is the reason you reached for it.
Almost none of it is here. Dump, restore, verify, manifest and fingerprint are
`shared/backup` in **`vantage-shared`**; `internal/cmd` holds argument parsing
and the words an operator reads. That split is what would let the control plane
import `shared/backup` later a scheduled in-process backup, say without a
import `shared/backup` later - a scheduled in-process backup, say - without a
second implementation to keep in sync. `shared/cryptobox` is the same move one
layer down: the single AES-256-GCM implementation, which the server delegates to
rather than keeping its own copy that `backup` would have had to duplicate to
@@ -38,8 +38,8 @@ decrypt a probe value during `verify`.
So **a fix to backup behaviour is usually a `vantage-shared` release plus a pin
bump here**, and it is not live until this repository cuts a tag. That is
deliberate an operator restoring a database should be running a version they
can name but it does mean the fix is not in anybody's hands the moment it
deliberate - an operator restoring a database should be running a version they
can name - but it does mean the fix is not in anybody's hands the moment it
merges.
## The refusals, and why each exists
@@ -50,28 +50,28 @@ each one guards something with no way back.
- **`backup` will not run without `KEY_ENCRYPTION_KEY`** unless `--allow-no-key`
is passed. The archive stores a SHA-256 **fingerprint** of the key, never the
key. An archive with no fingerprint cannot tell a later restore that the wrong
key is in hand it can only find that out when the data comes back as noise.
key is in hand - it can only find that out when the data comes back as noise.
- **`restore` refuses a non-empty target database, and there are no merge
semantics.** No code path upserts an archive's documents over existing ones:
merging two control planes reconciles nothing about which SSH keys are still
valid or which users still exist, and an upsert would resurrect a revoked key
or a deleted member from the older side.
- **`--force` needs a second assurance**: `--confirm-db NAME` matching the
target exactly, which works everywhere, or on a terminal only, and only when
`--confirm-db` was not given the database name typed back at a prompt.
target exactly, which works everywhere, or - on a terminal only, and only when
`--confirm-db` was not given - the database name typed back at a prompt.
`--confirm-db` is accepted on a terminal too, and is the stronger of the two:
naming the target in the command itself means a copied command carries its
intended target and cannot destroy a different one by accident. Without a
terminal and without `--confirm-db`, `--force` is refused.
- **`--force` drops only what the archive names.** Collections in the target
that the archive does not carry are left alone and **named in a warning** an
that the archive does not carry are left alone and **named in a warning** - an
archive taken with `--exclude workflow_log_lines`, restored over a live
database, leaves the old lines joined to restored runs and the operator must
be told. Dropping them instead would delete data nobody asked to delete.
## Three decisions that look like details and are not
- **Collections are enumerated live**, not read from a static list the
- **Collections are enumerated live**, not read from a static list - the
opposite of what instance-deletion purge does in the control plane. Purge must
never miss a tenant-scoped collection, so it keeps a hand-maintained registry;
a backup must never miss **any** collection, including the ones carrying no
@@ -92,7 +92,7 @@ each one guards something with no way back.
## The mirror that fails silently
**`backup.ciphertextFields` lives in `vantage-shared` and mirrors
`server/internal/models` in the `vantage` repository by hand** `shared/` cannot
`server/internal/models` in the `vantage` repository by hand** - `shared/` cannot
import it. The map naming each collection's `*_enc` fields (`keys`, `secrets`,
`auth_providers`, `console_sessions`) must change in the same commit as any of
those bson tags.
@@ -100,21 +100,21 @@ those bson tags.
Wrong field names are **silent**: `verify`'s live probe simply finds no
ciphertext and reports "this database stores no ciphertext yet", so the one gate
that catches what a key fingerprint cannot becomes a no-op. `settings` is
deliberately in neither that map nor `CiphertextCollections()` its ESO read
deliberately in neither that map nor `CiphertextCollections()` - its ESO read
token is a SHA-256 hash, not ciphertext.
## Building and releasing
`GOPRIVATE=gitea.hostxtra.co.uk/*` plus a credential, since `vantage-shared` is
private. CI writes a netrc from `REGISTRY_USER` + `RELEASE_TOKEN` in **both**
jobs they do not share a filesystem and the image build takes it as a
jobs - they do not share a filesystem - and the image build takes it as a
BuildKit secret rather than a build arg, which would survive in the builder
layer's history. **`RELEASE_TOKEN` needs read access to the `vantage` org.**
Tags are a bare **`v*`** now this is its own repository. The old
`vantagectl/v*` prefix namespaced one component's releases inside a shared
repository; unlike the agent's `agent/v*` which the control plane greps
release tag names for nothing reads this one programmatically. Releases from
repository; unlike the agent's `agent/v*` - which the control plane greps
release tag names for - nothing reads this one programmatically. Releases from
before the move keep their prefixed tags in the monorepo's release list.
```bash
@@ -128,5 +128,9 @@ Builds `linux/amd64`, `linux/arm64`, `darwin/arm64` and `windows/amd64`, writes
**The runtime stage is `scratch`, and it carries an explicitly copied `/tmp`.**
`restore` extracts an archive to a temporary directory before verifying its
checksums, and a scratch image has none for `os.MkdirTemp` to find. Without it
every restore fails the same omission costs the control plane only its
every restore fails - the same omission costs the control plane only its
vulnerability scanning, but here it is the whole tool.
## Writing style
Never use em dashes (the long dash character) anywhere: code, comments, UI copy, docs, commit messages. Use a plain hyphen ` - `, a comma, a colon, or split the sentence instead.
+1 -1
View File
@@ -27,7 +27,7 @@ RUN --mount=type=secret,id=netrc,target=/root/.netrc \
-ldflags="-s -w -X main.Version=${VERSION}" -o /vantagectl .
# Staged so the scratch image below can have a /tmp. It cannot mkdir one
# itself scratch has no shell.
# itself - scratch has no shell.
RUN mkdir -p /staging/tmp && chmod 1777 /staging/tmp
# Runtime stage
+1 -1
View File
@@ -39,7 +39,7 @@ func renderManifest(w io.Writer, m backup.Manifest) {
fmt.Fprintf(w, "Format version %d\n", m.FormatVersion)
if m.KeyFingerprint == nil {
fmt.Fprintf(w, "Key none recorded this archive cannot be checked "+
fmt.Fprintf(w, "Key none recorded - this archive cannot be checked "+
"against any KEY_ENCRYPTION_KEY\n")
} else {
fmt.Fprintf(w, "Key %s\n", *m.KeyFingerprint)
+2 -2
View File
@@ -102,8 +102,8 @@ func newRestoreCmd() *cobra.Command {
// confirmDestruction gates a --force restore.
//
// On a terminal the operator types the database name. Without one a
// Kubernetes Job, a CI step, a cron entry the same assurance comes from
// On a terminal the operator types the database name. Without one - a
// Kubernetes Job, a CI step, a cron entry - the same assurance comes from
// --confirm-db, whose value must equal the target. Naming the database in the
// argument means a copy-pasted command carries its intended target with it and
// cannot destroy a different one.
+2 -2
View File
@@ -31,8 +31,8 @@ func newVerifyCmd() *cobra.Command {
}
defer archive.Close()
// The key is read unconditionally. Archive-only mode no MONGO_URI,
// which is what a scheduled check uses must still compare the key
// The key is read unconditionally. Archive-only mode - no MONGO_URI,
// which is what a scheduled check uses - must still compare the key
// in hand against the archive's fingerprint; leaving it unset there
// reported "KEY_ENCRYPTION_KEY is not set" for a key that was set
// and correct.