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

This commit is contained in:
2026-09-10 09:18:58 +00:00
parent 759d36e85d
commit 059949674d
31 changed files with 65 additions and 61 deletions
+10 -6
View File
@@ -7,7 +7,7 @@ The private Go module every Vantage service imports. Extracted from the
vantage-shared/
├── grpc/pb/ # the agent↔control-plane wire types, hand-written
├── grpc/codec/ # the JSON codec they travel over
├── proto/vantage/v1/ # vantage.proto documentation for the above
├── proto/vantage/v1/ # vantage.proto - documentation for the above
├── mail/ # the one email system: transport + templates
├── license/ # payload, sign, verify, trusted keys, plans
├── models/ # Instance, User, Settings
@@ -18,7 +18,7 @@ vantage-shared/
└── cmd/lkctl/ # issue and inspect licences by hand
```
Module path is `gitea.hostxtra.co.uk/vantage/vantage-shared` **lowercase
Module path is `gitea.hostxtra.co.uk/vantage/vantage-shared` - **lowercase
`vantage`**, though the Gitea org is canonically `Vantage`. Gitea serves both
spellings; Go module paths are case-sensitive strings, and two spellings would
cache as two modules. Keep the lowercase one.
@@ -46,7 +46,7 @@ v0.2.0`. Consumers pin exact versions.
`grpc/codec`. Nothing generates them, and `vantage.proto` is not compiled by any
build. It is the readable statement of the wire contract, and it lives in the
same repository as the Go types precisely so that a message added to one can be
added to the other **in the same commit** that co-location is the only thing
added to the other **in the same commit** - that co-location is the only thing
enforcing the match, so do not split them again.
There used to be two copies of `pb`, in the agent and the server, and they had
@@ -57,7 +57,7 @@ sides. The agent links the server half as dead code, which the linker drops.
**A wire change lands in three steps, in this order**: release this module, bump
the pin in `vantage/server` (live at the next push to main), bump the pin in
`vantage/agent` (live only at the next `agent/v*` tag). The control plane will
be ahead of the fleet in between, which was true before too it is just written
be ahead of the fleet in between, which was true before too - it is just written
down in two `go.mod` files now instead of implied by a shared directory.
## Things that mirror something outside this repository
@@ -71,7 +71,7 @@ with a file in another repository:
probe 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 token is a SHA-256 hash, not
`CiphertextCollections()` - its ESO read token is a SHA-256 hash, not
ciphertext.
- **`mail/templates/layout.html.tmpl`** carries the control plane's dark theme
values as **literal hex**. Email clients support neither `var()` nor a
@@ -81,8 +81,12 @@ with a file in another repository:
- **`provision`** is the single implementation of the slug rules that both the
control plane and Vantage HQ depend on. It is the one place those two
repositories must agree on behaviour, which is why it is here rather than
copied but it now also means a change to it is a release and two pin bumps.
copied - but it now also means a change to it is a release and two pin bumps.
`mail/render_test.go` renders every template and fails if one exists that no
case covers. The templates are parsed in `init()`, so without that test a
mistyped field is a boot-time panic in three services.
## 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.
+2 -2
View File
@@ -98,8 +98,8 @@ func indexMember(name string) string { return "indexes/" + name + ".json" }
// Reader is an opened archive.
//
// Open extracts to a temporary directory rather than streaming, because gzip
// offers no random access and the manifest which carries the checksums every
// other member is judged against is written last. Verifying before writing a
// offers no random access and the manifest - which carries the checksums every
// other member is judged against - is written last. Verifying before writing a
// single document to the target is worth one pass over local disk. This is why
// the container image needs a /tmp.
type Reader struct {
+2 -2
View File
@@ -148,8 +148,8 @@ func dumpIndexes(ctx context.Context, w *Writer, db *mongo.Database, name string
defer cur.Close(ctx)
// The specs are read as raw BSON and re-encoded as extended JSON, one
// element per index, so key order and every option the server reported
// partialFilterExpression, collation, weights and the rest survive
// element per index, so key order and every option the server reported -
// partialFilterExpression, collation, weights and the rest - survive
// verbatim. Decoding into bson.M would lose compound key order, and
// reconstructing an index from a hand-picked set of options would drop
// whatever was not picked.
+4 -4
View File
@@ -72,7 +72,7 @@ func (o RestoreOptions) warn(format string, args ...any) {
// The order is fixed and every check that can refuse does so before the first
// write: format, checksums (done by Open), key policy, then target inspection.
// A restore that has begun writing and then fails leaves a partial database
// which the next run refuses to touch, which is correct the alternative is a
// which the next run refuses to touch, which is correct - the alternative is a
// silent merge, and merging two control planes reconciles nothing.
func Restore(ctx context.Context, opt RestoreOptions) (RestoreResult, error) {
m := opt.Archive.Manifest()
@@ -302,14 +302,14 @@ func splitBSON(raw []byte) (bson.Raw, []byte, error) {
// The specs are handed to the createIndexes command exactly as the source
// server reported them, rather than reconstructed into a mongo.IndexModel from
// a hand-picked set of options. Reconstruction dropped every option nobody had
// thought to pick partialFilterExpression above all, which this codebase
// thought to pick - partialFilterExpression above all, which this codebase
// relies on for partial unique indexes, and which replayed as a full unique
// index fails on any real database. It also lost compound key order, which is
// significant.
//
// A unique index that will not build means the restored data violates it, and
// the unique indexes here (instance_id, email), instance slug, settings
// instance, the ESO token hash are tenant-isolation properties rather than
// the unique indexes here - (instance_id, email), instance slug, settings
// instance, the ESO token hash - are tenant-isolation properties rather than
// optimisations. That aborts. A non-unique index failing is a performance
// problem and warns.
func replayIndexes(ctx context.Context, opt RestoreOptions, coll *mongo.Collection, name string) (int, error) {
+3 -3
View File
@@ -243,7 +243,7 @@ func TestRestoreAbortsWhenAUniqueIndexCannotBuild(t *testing.T) {
// Built by hand rather than dumped: two documents that collide on email
// alongside an index specification declaring email unique. No live database
// would let those coexist, which is exactly the point this is the shape
// would let those coexist, which is exactly the point - this is the shape
// of a corrupted or hand-edited archive, and restore must refuse rather
// than load the rows and leave the index missing.
a, err := bson.Marshal(bson.M{"email": "a@example.com"})
@@ -371,8 +371,8 @@ func TestIdIndexIsSkipped(t *testing.T) {
}
// TestRestoreReplaysPartialUniqueIndex is the regression guard for the defect
// that made a restore abort on any real database: a partial unique index
// this codebase has them on workflow_steps and settings replayed as a full
// that made a restore abort on any real database: a partial unique index -
// this codebase has them on workflow_steps and settings - replayed as a full
// unique index hits duplicate keys, and a failing unique index is fatal.
func TestRestoreReplaysPartialUniqueIndex(t *testing.T) {
client, _ := testDB(t)
+7 -7
View File
@@ -109,8 +109,8 @@ func probe(ctx context.Context, opt VerifyOptions, rep *VerifyReport) error {
rep.ProbeDecrypted = true
return nil
}
// No ciphertext anywhere is an ordinary state a deployment that has
// stored no secrets, keys or SSO configuration yet and is not a failure.
// No ciphertext anywhere is an ordinary state - a deployment that has
// stored no secrets, keys or SSO configuration yet - and is not a failure.
return nil
}
@@ -121,13 +121,13 @@ func probe(ctx context.Context, opt VerifyOptions, rep *VerifyReport) error {
// This map MIRRORS BY HAND the bson tags in server/internal/models, which this
// package cannot import: shared/ is a separate module and models is under
// server/internal. It must change in the same commit as any rename of the
// fields below the same mirrored-constant hazard as web/lib/targets.ts and
// fields below - the same mirrored-constant hazard as web/lib/targets.ts and
// services.MaxWorkloadLogLines. The sources are:
//
// keys models/key.go: private_key_enc, passphrase_enc
// secrets models/secret.go: encrypted_value
// auth_providers models/auth_provider.go: client_secret_enc
// console_sessions models/console_session.go: rdp_user_enc, rdp_pass_enc
// keys - models/key.go: private_key_enc, passphrase_enc
// secrets - models/secret.go: encrypted_value
// auth_providers - models/auth_provider.go: client_secret_enc
// console_sessions - models/console_session.go: rdp_user_enc, rdp_pass_enc
//
// settings is deliberately absent: it holds no ciphertext at all. The ESO read
// token is stored as a SHA-256 hash, which no key opens.
+2 -2
View File
@@ -45,7 +45,7 @@ const (
FeatureMCP = "mcp"
)
// Support levels. Carried for display and enforced by nothing there is no code
// Support levels. Carried for display and enforced by nothing - there is no code
// path anywhere that branches on these, and there must not be one. They are here
// so an air-gapped install can tell its operator who to call without reaching
// Vantage HQ.
@@ -74,7 +74,7 @@ type Limits struct {
// FillUnset replaces any zero field with the same field from base.
//
// This exists for one reason: a licence signed before a field existed decodes it
// as 0, and 0 would read as the most restrictive possible value no monitors,
// as 0, and 0 would read as the most restrictive possible value - no monitors,
// and an audit log trimmed to nothing. A blob we cannot re-sign must not be
// allowed to mean that.
//
+1 -1
View File
@@ -4,7 +4,7 @@ package license
//
// This table is the seed. The admin service owns the authoritative copy in its
// `plans` collection, and every issued licence snapshots the plan it was cut
// from so editing a plan never rewrites an existing licence, the same rule as
// from - so editing a plan never rewrites an existing licence, the same rule as
// workflow_runs.steps_snapshot.
//
// Limits here are the BASE allowance: what the tier grants before anything is
+1 -1
View File
@@ -97,7 +97,7 @@ func Verify(blob string, opts VerifyOpts) Result {
// Parse verifies the signature only, ignoring binding and expiry.
//
// Used to display a licence and to inspect a blob a customer has emailed in.
// Never use it for enforcement it does not check who the licence is for.
// Never use it for enforcement - it does not check who the licence is for.
func Parse(blob string) (License, error) {
parsed, err := lk.LicenseFromB32String(blob)
if err != nil {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"time"
)
// SendLicense delivers the blob inline. It is signed public data, not a secret
// SendLicense delivers the blob inline. It is signed public data, not a secret -
// it is useless on any instance other than the one it names.
func (s Sender) SendLicense(to, instanceName, blob string) error {
return s.sendTemplate(to, "", "license", struct {
+1 -1
View File
@@ -90,7 +90,7 @@ func normaliseText(s string) string {
}
// funcs are shared by both template flavours. They exist so that a message
// template never formats a date or builds a structure itself two templates
// template never formats a date or builds a structure itself - two templates
// formatting the same date two ways is exactly the drift this package removes.
var funcs = map[string]any{
// dict builds a map for the layout's helper templates, which take more
+3 -3
View File
@@ -3,7 +3,7 @@
// It owns three things that used to exist in three copies: the SMTP
// conversation (including the 465-implicit-TLS case that net/smtp gets wrong),
// the RFC 5322 envelope, and the rendered look of a Vantage email. Callers see
// only typed Send* methods nobody outside this package builds a subject line,
// only typed Send* methods - nobody outside this package builds a subject line,
// a MIME part or a colour.
package mail
@@ -24,7 +24,7 @@ import (
// timeout bounds the whole SMTP conversation. Without it a mail server that
// accepts the connection and then stalls holds an HTTP request open until the
// client gives up and admin's signup rollback runs on that request's context.
// client gives up - and admin's signup rollback runs on that request's context.
const timeout = 15 * time.Second
// Sender is a configured SMTP destination. It is a value, not a singleton:
@@ -92,7 +92,7 @@ func (s Sender) sendTemplate(to, replyTo, name string, data any) error {
// Port 465 is implicit TLS: the server expects a TLS handshake immediately, so
// the connection is wrapped BEFORE any SMTP is spoken. Every other port gets
// plaintext then STARTTLS if offered. net/smtp.SendMail only does the latter,
// which is why it fails against a 465 mail server that bug silently stopped
// which is why it fails against a 465 mail server - that bug silently stopped
// every admin email from being delivered once already.
func (s Sender) send(m message) error {
if !s.Enabled() {
+1 -1
View File
@@ -3,5 +3,5 @@
{{template "lead" (printf "You have been invited to join %s on Vantage." .AccountName)}}
{{template "p" "Set your own password and finish joining:"}}
{{template "button" (dict "label" "Set password and join" "url" .Link)}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it nothing happens until you open the link."}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it - nothing happens until you open the link."}}
{{end}}
+1 -1
View File
@@ -4,5 +4,5 @@
{{template "lead" (printf "You have been invited to join %s on Vantage." .AccountName)}}
{{template "p" "Set your own password and finish joining:"}}
{{template "button" (dict "label" "Set password and join" "url" .Link)}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it nothing happens until you open the link."}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it - nothing happens until you open the link."}}
{{end}}
+3 -3
View File
@@ -3,7 +3,7 @@
Every colour in the email system lives in this file and nowhere else, in the
same way no component in web/, site/ or adminsite/ carries a hex. The values
are web/app/globals.css's tokens an email is read before the recipient
are web/app/globals.css's tokens - an email is read before the recipient
clicks through to the control plane, so the two should not look like
different products. They are written as literal hex here because email
clients support neither var() nor a reliable prefers-color-scheme, so the
@@ -18,7 +18,7 @@
--ink-3 #71879f --well #04101f
Layout is tables and inline styles throughout, which is not a stylistic
choice it is the only thing Outlook renders predictably.
choice - it is the only thing Outlook renders predictably.
A message file overrides "title", "pill" and "body"; the empty defaults below
exist so that a message needing no pill does not have to define one.
@@ -57,7 +57,7 @@
</p>
{{- end -}}
{{- /* well shows machine output a licence blob, an install ID. Mirrors
{{- /* well shows machine output - a licence blob, an install ID. Mirrors
web/'s --well surface, the floor beneath the ground. */ -}}
{{- define "well" -}}
<pre style="margin:0 0 20px;padding:14px;background:#04101f;border:1px solid #1e3855;border-radius:4px;color:#9fb3ca;font-family:ui-monospace,'Cascadia Mono','SF Mono',Menlo,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all;">{{.}}</pre>
+1 -1
View File
@@ -1,7 +1,7 @@
{{- /*
The plain-text counterpart of layout.html.tmpl.
It defines the same helper names p, lead, button, well, note, rows, chip
It defines the same helper names - p, lead, button, well, note, rows, chip -
so a message's txt file reads as the same document as its html one, and a
helper added on one side is obvious by its absence on the other.
+1 -1
View File
@@ -3,5 +3,5 @@
{{template "lead" (printf "Your licence for %s is below." .InstanceName)}}
{{template "p" "Paste it into Settings → Licence on your Vantage install:"}}
{{template "well" .Blob}}
{{template "p" "The licence is signed public data, not a secret it is useless on any instance other than the one it names."}}
{{template "p" "The licence is signed public data, not a secret - it is useless on any instance other than the one it names."}}
{{end}}
+1 -1
View File
@@ -4,5 +4,5 @@
{{template "lead" (printf "Your licence for %s is below." .InstanceName)}}
{{template "p" "Paste it into Settings > Licence on your Vantage install:"}}
{{template "well" .Blob}}
{{template "p" "The licence is signed public data, not a secret it is useless on any instance other than the one it names."}}
{{template "p" "The licence is signed public data, not a secret - it is useless on any instance other than the one it names."}}
{{end}}
+1 -1
View File
@@ -2,5 +2,5 @@
{{define "title"}}{{.InstanceName}} is renewed{{end}}
{{define "body"}}
{{template "lead" (printf "Your Free licence for %s now runs until %s." .InstanceName (date .Expires))}}
{{template "p" "Nothing else changes your servers, agents and monitors carry on as they were."}}
{{template "p" "Nothing else changes - your servers, agents and monitors carry on as they were."}}
{{end}}
+1 -1
View File
@@ -3,5 +3,5 @@
{{define "title"}}{{.InstanceName}} is renewed{{end}}
{{define "body"}}
{{template "lead" (printf "Your Free licence for %s now runs until %s." .InstanceName (date .Expires))}}
{{template "p" "Nothing else changes your servers, agents and monitors carry on as they were."}}
{{template "p" "Nothing else changes - your servers, agents and monitors carry on as they were."}}
{{end}}
+1 -1
View File
@@ -3,5 +3,5 @@
{{template "lead" "Confirm this address to finish setting up your Vantage account."}}
{{template "button" (dict "label" "Confirm email address" "url" .Link)}}
{{template "p" (printf "The link works once and expires in %d hours." .TTLHours)}}
{{template "p" "If you did not request this, ignore this email nothing happens until the link is opened."}}
{{template "p" "If you did not request this, ignore this email - nothing happens until the link is opened."}}
{{end}}
+1 -1
View File
@@ -4,5 +4,5 @@
{{template "lead" "Confirm this address to finish setting up your Vantage account."}}
{{template "button" (dict "label" "Confirm email address" "url" .Link)}}
{{template "p" (printf "The link works once and expires in %d hours." .TTLHours)}}
{{template "p" "If you did not request this, ignore this email nothing happens until the link is opened."}}
{{template "p" "If you did not request this, ignore this email - nothing happens until the link is opened."}}
{{end}}
+1 -1
View File
@@ -6,7 +6,7 @@
(dict "k" "Instance" "v" .InstanceName)
(dict "k" "New findings" "v" .Count))}}
{{range .Rows}}
{{if .FixedIn}}{{template "well" (printf "%s (%s) %s on %s, fixed in %s" .CVEID .Severity .PackageName .ServerName .FixedIn)}}{{else}}{{template "well" (printf "%s (%s) %s on %s, no fix published" .CVEID .Severity .PackageName .ServerName)}}{{end}}
{{if .FixedIn}}{{template "well" (printf "%s (%s) - %s on %s, fixed in %s" .CVEID .Severity .PackageName .ServerName .FixedIn)}}{{else}}{{template "well" (printf "%s (%s) - %s on %s, no fix published" .CVEID .Severity .PackageName .ServerName)}}{{end}}
{{end}}
{{if .More}}{{template "p" (printf "…and %d more." .More)}}{{end}}
{{template "note" (printf "Scanned against a vulnerability database pulled %s ago." .DBAge)}}
+1 -1
View File
@@ -4,7 +4,7 @@
{{define "body"}}
{{template "lead" .Summary}}
{{range .Rows}}- {{.CVEID}} ({{.Severity}}) {{.PackageName}} on {{.ServerName}}{{if .FixedIn}}, fixed in {{.FixedIn}}{{else}}, no fix published{{end}}
{{range .Rows}}- {{.CVEID}} ({{.Severity}}) - {{.PackageName}} on {{.ServerName}}{{if .FixedIn}}, fixed in {{.FixedIn}}{{else}}, no fix published{{end}}
{{end}}
{{if .More}}...and {{.More}} more.{{end}}
+2 -2
View File
@@ -10,7 +10,7 @@ type VulnDigestRow struct {
PackageName string
ServerName string
// FixedIn empty means no vendor fix has been published, which the template
// says explicitly rather than leaving blank it is a real state, not
// says explicitly rather than leaving blank - it is a real state, not
// missing data.
FixedIn string
}
@@ -23,7 +23,7 @@ type VulnDigestRow struct {
type VulnDigest struct {
InstanceName string
// Count is every newly opened finding in the batch, which may exceed
// len(Rows) Rows is capped and More carries the remainder.
// len(Rows) - Rows is capped and More carries the remainder.
Count int
TopSeverity string
Summary string
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// keys, workflows, monitors and secrets. It is the unit a licence attaches to.
//
// A paying customer may hold several. That grouping is called an Account and
// lives only in the admin control plane this service never sees it.
// lives only in the admin control plane - this service never sees it.
type Instance struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
InstanceID string `bson:"instance_id" json:"instance_id"`
+2 -2
View File
@@ -31,7 +31,7 @@ type Settings struct {
// LocalLoginEnabled is a pointer because it is absent on every settings
// document written before this feature existed, and a plain bool would read
// absent as disabled turning off password login for the entire fleet at
// absent as disabled - turning off password login for the entire fleet at
// upgrade. Nil means enabled.
LocalLoginEnabled *bool `bson:"local_login_enabled,omitempty" json:"local_login_enabled,omitempty"`
@@ -43,7 +43,7 @@ type Settings struct {
// APITokenMaxDays caps how long a newly created API token may live.
//
// A pointer for the same reason the retention fields are: absent must mean
// the default, and the default here is no cap at all never-expire tokens
// the default, and the default here is no cap at all - never-expire tokens
// are allowed until an instance decides otherwise, so an upgrade changes
// nothing. Nil or 0 is no cap. A positive value refuses both a longer
// expiry and a token with no expiry.
+1 -1
View File
@@ -26,7 +26,7 @@ const (
AuthOIDC = "oidc"
// AuthHQ marks a user projected from a Vantage HQ account. Its role,
// password and existence are owned by HQ, and the instance API refuses to
// change any of them locally a role editable in two places is a role with
// change any of them locally - a role editable in two places is a role with
// two answers.
AuthHQ = "hq"
)
+2 -2
View File
@@ -42,7 +42,7 @@ message SyncResponse {
// collect_packages tells the agent whether this instance's licence grants
// vulnerability scanning. False means do not collect at all: no gRPC body,
// no document, no storage. The server re-checks on ReportPackages this
// no document, no storage. The server re-checks on ReportPackages - this
// flag is the optimisation, the server check is the boundary.
//
// Absent reads as false, which is the safe direction: an old server that
@@ -246,7 +246,7 @@ message ServerCommand {
// Behind an L7 proxy the agent's HTTP/2 connection terminates at the proxy, so
// keepalive pings are answered by the proxy whether or not the server behind it
// is still there. A pod that dies leaves the agent blocked in Recv on a stream
// that will never produce another message and never error commands are
// that will never produce another message and never error - commands are
// dispatched into it and silently lost. Only traffic that originates at the
// server itself distinguishes a live stream from an orphaned one.
message PingCmd {
+5 -5
View File
@@ -30,8 +30,8 @@ func CreateInstance(ctx context.Context, db *mongo.Database, name string) (*mode
// created before payment and provisioning happens on the confirmed-payment
// webhook. Provisioning with the placeholder's own ID keeps the id stable, so
// the subscription's custom_data never points at a rewritten row and later
// webhooks still resolve it. If an instance with this ID already exists a
// webhook retried after a partial provision it is returned as-is rather than
// webhooks still resolve it. If an instance with this ID already exists - a
// webhook retried after a partial provision - it is returned as-is rather than
// duplicated.
//
// The count-then-insert loop is racy on its own. It is safe only because
@@ -92,7 +92,7 @@ var ErrSlugTaken = errors.New("slug taken")
// the slug it holds now.
//
// It returns the current slug unchanged when the name still derives to it, so a
// cosmetic edit capitalisation, punctuation, a trailing "Ltd." is not a move
// cosmetic edit - capitalisation, punctuation, a trailing "Ltd." - is not a move
// and cannot collide with the instance's own slug.
func RenameSlug(name, currentSlug string) (string, error) {
base, err := BaseSlug(name)
@@ -110,13 +110,13 @@ func RenameSlug(name, currentSlug string) (string, error) {
// It returns the name and slug the control plane held BEFORE the write, and
// those are the only correct values to unwind with. The caller's own copy of the
// instance may be stale, and admin's copy stores slug with `omitempty`, so an
// unwind driven from there can write an empty slug which either mis-restores
// unwind driven from there can write an empty slug - which either mis-restores
// the tenant host or trips the unique index against every other slugless row.
//
// The count-then-update is racy on its own, and is safe for the same reason
// CreateInstanceWithID's loop is: instances.slug carries a unique index, so a
// lost race surfaces as a duplicate-key error. Unlike creation there is nothing
// to retry with the caller asked for one specific name so it becomes
// to retry with - the caller asked for one specific name - so it becomes
// ErrSlugTaken. Do not remove the duplicate-key branch, and do not remove the
// index.
func RenameInstance(ctx context.Context, db *mongo.Database, instanceID, name string) (inst *models.Instance, prevName, prevSlug string, err error) {
+1 -1
View File
@@ -2,7 +2,7 @@
// plane and sitesvc.
//
// These rules used to be duplicated: the control plane owned one copy and
// sitesvc mirrored it by hand. The copies had already drifted sitesvc retried
// sitesvc mirrored it by hand. The copies had already drifted - sitesvc retried
// on a lost slug race while the control plane returned an error. This package
// is the single definition; neither service may reimplement any of it.
package provision