🤖 Fix wave: a command they can run, a claim that is true, and fewer antitheses
Five findings from the whole-branch review, one pass. The Trash paragraph told a hosted customer to run `carlos add <host>` — a box-local operator verb built around `-registry`, which a customer has no box for. It now names `carlos instances create`, the verb the CLI itself recommends in the same situation (deploy.go:262), and says plainly that the console's own reply string still says otherwise so a reader who has just seen that message knows why. The console string is a platform bug, filed as carlosframework/platform#327. "The platform does not send mail to anyone" stopped being true when tenant email went live, and contradicted the console page on this same site. Scoped to the narrow claim that is still true — inviting somebody sends them no email — with the fallback the code actually has when Keymail delivery fails. Flattened 13 of the 25 X-rather-than-Y / X-not-Y sentences, keeping the ones where the contrast is the point: "a lost secret means rotating, not re-running", "inventory, not history", "one timeline rather than two", "close to live, not exactly live", "make a stolen token boring, not ration ordinary work". members.md 8 -> 3, limits.md 5 -> 2, logs.md 4 -> 2, corpus 25 -> 12. ", which" came down 13 -> 10 in the same edits. check-docs.mjs blamed a `sync-docs` script this repo does not have — carried in from the sibling, where the corpus is vendored. This one is hand-authored, so all four messages now describe a hand-edited docsnav.json that is malformed, empty, or out of step with src/docs/. And the docs used hello.q4t.oncarlos.com while the landing page and /get-started/ use hello.bdf — same site, two sqids. Both docs occurrences now match the rest of the site. npm run check exits 0 on a clean build; index.html and get-started/ stay byte-identical to 7123624.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9 files changed,
+53
−41
hack/check-docs.mjs+9 −7src/docs/config.md+1 −1src/docs/console.md+16 −5src/docs/deploying.md+2 −3src/docs/domains.md+1 −1src/docs/limits.md+4 −4src/docs/logs.md+3 −3src/docs/members.md+15 −15src/docs/sleeping.md+2 −2
diff --git a/hack/check-docs.mjs b/hack/check-docs.mjs| index e9e6a56..ef3ae9d 100644 |
| --- a/hack/check-docs.mjs |
| +++ b/hack/check-docs.mjs |
| @@ -69,16 +69,18 @@ for (const rel of requiredSitePaths) { |
| // 2. Every nav entry has a built page and a built .md twin. |
| // |
| -// The floor below is the point of the whole gate: a loop over an empty |
| -// nav asserts nothing, so a sync-docs regression that wrote |
| -// {"sections": []} would print "docs ok — 0 pages" and exit 0 on a |
| -// corpus that had vanished. Same shape as the pages.length guard below. |
| +// docsnav.json is hand-maintained here — there is no generator to blame |
| +// or re-run, so every failure below is an edit someone made. The floor |
| +// after the loop is the point of the whole gate: a loop over an empty nav |
| +// asserts nothing, so a nav trimmed down to {"sections": []} would print |
| +// "docs ok — 0 pages" and exit 0 on a corpus that had vanished. Same shape |
| +// as the pages.length guard below. |
| const slugs = []; |
| const sections = Array.isArray(nav.sections) ? nav.sections : []; |
| if (!Array.isArray(nav.sections)) fail("docsnav.json has no sections array at all"); |
| for (const section of sections) { |
| if (!Array.isArray(section.pages)) { |
| - fail(`docsnav.json section ${JSON.stringify(section.title ?? "?")} has no "pages" key — sync-docs has probably written a malformed file`); |
| + fail(`docsnav.json section ${JSON.stringify(section.title ?? "?")} has no "pages" key — every section in src/_data/docsnav.json needs one, listing that section's pages`); |
| continue; |
| } |
| for (const entry of section.pages) { |
| @@ -90,7 +92,7 @@ for (const section of sections) { |
| } |
| } |
| if (slugs.length === 0) { |
| - fail("docsnav.json lists no pages — sync-docs has not run, or has failed; nothing in check 2 can fail against an empty nav"); |
| + fail("src/_data/docsnav.json lists no pages — every page in src/docs/ needs an entry there; nothing in check 2 can fail against an empty nav"); |
| } |
| if (!exists(join(docs, "index.html"))) fail("no built /docs/ index"); |
| @@ -156,7 +158,7 @@ const { slugify } = await import("../eleventy.config.js"); |
| const fixture = JSON.parse(readFileSync("src/_data/docsanchors.json", "utf8")); |
| const cases = Object.entries(fixture).filter(([input]) => input !== "//"); |
| if (cases.length < 5) { |
| - fail("anchor fixture has too few cases to pin anything — did sync-docs run?"); |
| + fail("src/_data/docsanchors.json has too few cases to pin anything — it has been emptied or truncated; restore it from the platform repo's docs/site/anchors.json"); |
| } |
| for (const [input, want] of cases) { |
| const got = slugify(input); |
diff --git a/src/docs/config.md b/src/docs/config.md| index afe82d8..9de659f 100644 |
| --- a/src/docs/config.md |
| +++ b/src/docs/config.md |
| @@ -39,7 +39,7 @@ cannot itself open. |
| The consequence is the one thing people trip over: nothing can show you |
| the value again. `carlos secrets list` prints key names and stops there. |
| -Lose the value and you set a new one rather than recovering the old one. |
| +Lose the value and your only move is setting a new one. |
| One practical wrinkle. The value is on your command line when you type |
| it, so your shell history keeps a copy of it. Clear that out on a machine |
diff --git a/src/docs/console.md b/src/docs/console.md| index edbdd85..7834a10 100644 |
| --- a/src/docs/console.md |
| +++ b/src/docs/console.md |
| @@ -59,11 +59,22 @@ removing one and handing over ownership live on the Team page; minting, |
| rotating and revoking a credential a server holds live on Account settings. |
| Deletion is the other cluster. A deleted app goes to Trash and stays |
| -restorable for 30 days, keeping its releases, channels and history — |
| -restoring gives all of that back with no routes, so run |
| -`carlos add <host>` afterwards to serve it again. An owner can also destroy |
| -one immediately from the same page, which asks for the app's name typed out |
| -and then for your passkey. |
| +restorable for 30 days, keeping its releases, channels and history. |
| +Restoring gives all of that back with no routes, so the app comes back with |
| +nowhere to serve from until you make one: |
| + |
| +```sh |
| +carlos instances create -app hello -host hello.bdf.oncarlos.com |
| +``` |
| + |
| +That is the same command [Deploying](/docs/deploying/) names when a channel |
| +has no instance following it. The console's own confirmation currently tells |
| +you to run `carlos add` instead — don't. That verb edits a box's route |
| +registry from the box, and your app doesn't come with a box you can log in |
| +to. |
| + |
| +An owner can also destroy an app immediately from the same page; that asks |
| +for the app's name typed out and then for your passkey. |
| And when the day's app-creation budget is spent, the console's Create app |
| form is the way through — it has no limit of its own. [Limits and getting |
diff --git a/src/docs/deploying.md b/src/docs/deploying.md| index ab12f05..9d95de7 100644 |
| --- a/src/docs/deploying.md |
| +++ b/src/docs/deploying.md |
| @@ -32,7 +32,7 @@ preferring over anything else you might reach for. A page that looks |
| right can be a cached copy of the old build: |
| ```sh |
| -curl -sI https://hello.q4t.oncarlos.com | grep -i x-carlos-version |
| +curl -sI https://hello.bdf.oncarlos.com | grep -i x-carlos-version |
| ``` |
| ## What you ship |
| @@ -49,8 +49,7 @@ this platform is: |
| GOOS=linux GOARCH=arm64 go build -o hello . |
| ``` |
| -A directory of files is a site rather than a binary, and takes |
| -`-kind static`: |
| +A directory of files ships as a site, with `-kind static`: |
| ```sh |
| carlos deploy -app hello-site -kind static ./public |
diff --git a/src/docs/domains.md b/src/docs/domains.md| index f2775fc..26bc375 100644 |
| --- a/src/docs/domains.md |
| +++ b/src/docs/domains.md |
| @@ -1,7 +1,7 @@ |
| # Custom domains |
| Your app already answers on a hostname the platform gave it, something |
| -like `hello.q4t.oncarlos.com`. Attaching your own domain is two moves: |
| +like `hello.bdf.oncarlos.com`. Attaching your own domain is two moves: |
| tell Carloku the hostname is yours, then point a DNS record at it. The |
| platform hostname keeps working afterwards, so you can do all of this |
| before you switch any traffic over. |
diff --git a/src/docs/limits.md b/src/docs/limits.md| index 9c7f36d..27f13d5 100644 |
| --- a/src/docs/limits.md |
| +++ b/src/docs/limits.md |
| @@ -29,11 +29,11 @@ get sixty a minute on their own budget, because a burst of signups creating |
| instances is the workload that API exists for. |
| Claiming an app name is the tight one: ten attempts a day per address, and |
| -attempts, not successes, so refused tries count. A claimed name persists |
| +a try that gets refused still spends one of them. A claimed name persists |
| whether or not you ever use it, which is why this ceiling runs over a day and |
| the others run over a minute. When you hit it, the message points at the |
| console's Create app form, which has no limit of its own. That is the |
| -intended way through, not a loophole. |
| +intended way through. |
| ## Credentials a server holds |
| @@ -43,8 +43,8 @@ twenty other writes a minute, a hundred app claims a day. A CI fleet cutting |
| releases back to back therefore cannot starve the person who minted it, and |
| the person's noisy afternoon cannot stop CI. |
| -The counters key on the credential's name, not its secret, so rotating one |
| -does not hand it a fresh budget. [Members and service |
| +The counters key on the credential's name, so rotating its secret does not |
| +hand it a fresh budget. [Members and service |
| credentials](/docs/members/) covers the rest of how they work. |
| ## Things that expire |
diff --git a/src/docs/logs.md b/src/docs/logs.md| index 928a804..a47f8bd 100644 |
| --- a/src/docs/logs.md |
| +++ b/src/docs/logs.md |
| @@ -55,10 +55,10 @@ site. |
| Seven days, and a size ceiling per app that drops the oldest hour first if |
| an app is unusually chatty. If you need a longer record than that, have the |
| -app ship its own copy somewhere; the platform's retention is for debugging |
| -this week, not for keeping an audit trail. |
| +app ship its own copy somewhere; the platform's retention is sized for |
| +debugging this week. |
| -An empty result is worth a second look rather than a bug report. Platform |
| +An empty result is usually honest, and worth a second look. Platform |
| events only happen when something happens — a wake, a restart, a failure — |
| so a quiet hour of a quiet app really does have nothing in it. Widen to |
| `-since 24h` before you conclude that logging is broken. |
diff --git a/src/docs/members.md b/src/docs/members.md| index 69c98b1..f5a450d 100644 |
| --- a/src/docs/members.md |
| +++ b/src/docs/members.md |
| @@ -9,10 +9,11 @@ they invite are different too. |
| ## People |
| Invites go out from the Team page in the console, addressed to a Keymail |
| -address. The invitation lands in that person's Keymail Apps inbox rather than |
| -their email; the platform does not send mail to anyone. It works once and |
| -expires after seven days, and an owner can revoke a pending one before it is |
| -used. |
| +address. The invitation lands in that person's Keymail Apps inbox — the same |
| +sealed path a sign-in confirmation takes — so inviting somebody sends them no |
| +email. It works once and expires after seven days, and an owner can revoke a |
| +pending one before it is used. If the delivery itself fails the console hands |
| +you the link to pass on yourself; the invitation is real by then either way. |
| There are two roles. A member does the day-to-day work on the account's apps |
| — deploy, promote, roll back, config, domains, logs. An owner does all of |
| @@ -51,16 +52,15 @@ Pick the narrowest role that does the job. `publish` is the deploy loop: |
| claim an app, ship, promote, roll back, manage hostnames, retire. `operate` |
| is runtime work with no release authority — restart, config, logs. `instance` |
| is the older app-token shape, kept for what already depends on it. `admin` is |
| -the union of those plus ledgers, storage and placement, and it includes |
| -granting object-store access, which mints a real IAM user. That last one is a |
| -handover, not a convenience. |
| +the union of those plus ledgers, storage and placement. It also grants |
| +object-store access, and that mints a real IAM user — hand one out only when |
| +you mean to hand over. |
| -`-app hello` binds the credential to one app, which also means it cannot |
| -claim new ones. Leave it off for an account-wide credential. Either way the |
| -reach is fixed at mint: if you join another account next month, the |
| -credential you minted does not follow you there. It draws on its own rate |
| -budgets rather than yours, too — see [Limits](/docs/limits/) — and it never |
| -expires, so revoking is the control. |
| +`-app hello` binds the credential to one app; it cannot claim new ones. |
| +Leave it off for an account-wide credential. Either way the reach is fixed at |
| +mint: if you join another account next month, the credential you minted does |
| +not follow you there. Its rate budgets are its own, separate from yours — see |
| +[Limits](/docs/limits/) — and it never expires, so revoking is the control. |
| ## Rotate replaces, revoke stops |
| @@ -73,7 +73,7 @@ carlos services rotate ci-runner |
| Rotation mints a fresh secret and prints it once, keeping the name, the role, |
| the scope and the history. The old secret is refused on the credential's |
| *next* call — work already in flight is unaffected. That makes rotation a |
| -hygiene move, not an incident response. |
| +hygiene move: something you schedule. |
| ```sh |
| carlos services revoke ci-runner |
| @@ -95,7 +95,7 @@ carlos services ls |
| Live credentials only — name, role, scope, when it was minted, when its |
| current secret began, when it was last used, and who minted it. A revoked one |
| -is gone rather than listed as revoked, so this is inventory, not history. |
| +disappears from the list entirely, so this is inventory, not history. |
| Every verb here is owner-only, and closed to service credentials themselves: |
| no credential can mint, rotate or revoke any credential, not even its own. |
diff --git a/src/docs/sleeping.md b/src/docs/sleeping.md| index 127aa51..505f9d2 100644 |
| --- a/src/docs/sleeping.md |
| +++ b/src/docs/sleeping.md |
| @@ -50,8 +50,8 @@ together during a wake all join the same one instead of each starting their |
| own. |
| The exception is a wake that takes an unusual length of time. Past about |
| -thirty seconds the request is answered with a 503 and a `Retry-After` rather |
| -than held open forever, and a wake that fails outright ends the same way. In |
| +thirty seconds the request is answered with a 503 and a `Retry-After`, and a |
| +wake that fails outright ends the same way. In |
| both cases the reason is one line in the platform stream: |
| ```sh |