settings-2 task 1: internal/updatewords, the draft strings for the settings plans
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+142
−0
internal/updatewords/words.go+50 −0internal/updatewords/words_authorize.go+46 −0internal/updatewords/words_test.go+46 −0
diff --git a/internal/updatewords/words.go b/internal/updatewords/words.go| new file mode 100644 |
| index 000000000..7f9d5d576 |
| --- /dev/null |
| +++ b/internal/updatewords/words.go |
| @@ -0,0 +1,50 @@ |
| +// Package updatewords holds every member-facing string the instance update |
| +// settings plans add (plans 2 to 5), as drafts with an ID and a context. The |
| +// copy review (plan 5, Task 1) exports All(), Paul rewrites them, and the |
| +// approved text replaces each Text; a test then pins them to the approved |
| +// record. Code reads a string only through Text, so a reviewed string has one |
| +// home. Each plan adds its own words_*.go file, so parallel branches never |
| +// edit the same lines. |
| +package updatewords |
| + |
| +import ( |
| + "fmt" |
| + "sort" |
| +) |
| + |
| +// Draft is one string: Context says where it appears and what it does, for |
| +// the reviewer who cannot see the screen. |
| +type Draft struct{ ID, Text, Context string } |
| + |
| +var drafts = map[string]Draft{} |
| + |
| +// add registers drafts from a file's init. A duplicate ID is a programming |
| +// error and panics at start-up, never at the moment a member reads it. |
| +func add(ds ...Draft) { |
| + for _, d := range ds { |
| + if _, dup := drafts[d.ID]; dup { |
| + panic(fmt.Sprintf("updatewords: duplicate id %s", d.ID)) |
| + } |
| + drafts[d.ID] = d |
| + } |
| +} |
| + |
| +// Text is the string for id. An unknown id panics: it can only be a typo in |
| +// code, which the package's tests catch. |
| +func Text(id string) string { |
| + d, ok := drafts[id] |
| + if !ok { |
| + panic(fmt.Sprintf("updatewords: unknown id %s", id)) |
| + } |
| + return d.Text |
| +} |
| + |
| +// All is every draft, sorted by ID. |
| +func All() []Draft { |
| + out := make([]Draft, 0, len(drafts)) |
| + for _, d := range drafts { |
| + out = append(out, d) |
| + } |
| + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) |
| + return out |
| +} |
diff --git a/internal/updatewords/words_authorize.go b/internal/updatewords/words_authorize.go| new file mode 100644 |
| index 000000000..e1daacab9 |
| --- /dev/null |
| +++ b/internal/updatewords/words_authorize.go |
| @@ -0,0 +1,46 @@ |
| +package updatewords |
| + |
| +func init() { |
| + // TODO-COPY: drafts awaiting Paul's copy review (copy-review/strings.json). |
| + add( |
| + Draft{ID: "settings.evidence.no-record", |
| + Text: "no promotion record: authorize it or pick a newer build", |
| + Context: "Why a pin, an install request or a recovery onto an older build was refused: the build was promoted before promotion records existed, or never promoted. Shown in carlos instances list, on the instance page, and returned by the console when a pin is set."}, |
| + Draft{ID: "settings.authorize.intro", |
| + Text: "Authorize %s %s with this manifest:", |
| + Context: "First line of carlos releases authorize, above the digests. App and version."}, |
| + Draft{ID: "settings.authorize.manifest-row", |
| + Text: " manifest %s", |
| + Context: "The manifest's sha256, the one digest the signed record will carry."}, |
| + Draft{ID: "settings.authorize.artifact-row", |
| + Text: " %s %s", |
| + Context: "One line per file of the build: its platform (or site) and its sha256, so the person can check them against what they built."}, |
| + Draft{ID: "settings.authorize.prompt", |
| + Text: "Sign a promotion record for exactly these bytes? Type yes to continue: ", |
| + Context: "Asked after the digests. Anything but yes signs nothing. --yes skips it."}, |
| + Draft{ID: "settings.authorize.done", |
| + Text: "authorized %s %s: it can now be pinned and recovered on any box", |
| + Context: "Printed after a record is signed. App and version."}, |
| + Draft{ID: "settings.authorize.already", |
| + Text: "%s %s already has a promotion record for these bytes; nothing to sign", |
| + Context: "Printed when the build already has a record with the same digest."}, |
| + Draft{ID: "settings.authorize.aborted", |
| + Text: "nothing signed", |
| + Context: "Printed when the person did not type yes."}, |
| + Draft{ID: "settings.authorize.changed", |
| + Text: "the manifest of %s changed since it was shown. Nothing was signed. Run the command again", |
| + Context: "Returned when someone reshipped the version name between showing the digests and the confirmation. %s is the version."}, |
| + Draft{ID: "settings.authorize.root-only", |
| + Text: "authorizing a build needs the deployment's root key. A scoped key cannot do it", |
| + Context: "Returned by carlos releases authorize against a bucket directly with a scoped CARLOS_RELEASE_KEY."}, |
| + Draft{ID: "settings.authorize.not-shipped", |
| + Text: "%s has not been shipped, or its manifest cannot be read", |
| + Context: "Returned when the version named does not exist. %s is the version."}, |
| + Draft{ID: "settings.authorize.no-key", |
| + Text: "this console has no release key, so it cannot sign a promotion record", |
| + Context: "Returned by the console's authorize door when the console was started without the deployment's release key. Shown by carlos releases authorize."}, |
| + Draft{ID: "settings.authorize.write-failed", |
| + Text: "could not write the promotion record. Nothing was signed. Try again", |
| + Context: "Returned by the console's authorize door when the bucket refused the record write. Shown by carlos releases authorize."}, |
| + ) |
| +} |
diff --git a/internal/updatewords/words_test.go b/internal/updatewords/words_test.go| new file mode 100644 |
| index 000000000..0d452269e |
| --- /dev/null |
| +++ b/internal/updatewords/words_test.go |
| @@ -0,0 +1,46 @@ |
| +package updatewords |
| + |
| +import ( |
| + "sort" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestDraftsAreWellFormed(t *testing.T) { |
| + all := All() |
| + if len(all) == 0 { |
| + t.Fatal("no drafts registered") |
| + } |
| + if !sort.SliceIsSorted(all, func(i, j int) bool { return all[i].ID < all[j].ID }) { |
| + t.Fatal("All is not sorted by ID") |
| + } |
| + for _, d := range all { |
| + if !strings.HasPrefix(d.ID, "settings.") || d.Text == "" || d.Context == "" { |
| + t.Errorf("%q: every draft needs a settings.* ID, a text and a context", d.ID) |
| + } |
| + if strings.ContainsRune(d.Text, '—') { |
| + t.Errorf("%s: no em dashes in member-facing text", d.ID) |
| + } |
| + if Text(d.ID) != d.Text { |
| + t.Errorf("%s: Text disagrees with All", d.ID) |
| + } |
| + } |
| +} |
| + |
| +func TestUnknownIDPanics(t *testing.T) { |
| + defer func() { |
| + if recover() == nil { |
| + t.Fatal("an unknown ID did not panic") |
| + } |
| + }() |
| + Text("settings.no.such.string") |
| +} |
| + |
| +func TestDuplicateIDPanics(t *testing.T) { |
| + defer func() { |
| + if recover() == nil { |
| + t.Fatal("a duplicate ID did not panic") |
| + } |
| + }() |
| + add(Draft{ID: "settings.authorize.done", Text: "x", Context: "y"}) |
| +} |