# Style

URL: https://docs.hanzo.ai/docs/contributing/style

> How code and prose are written here — one base URL, one key, one way to do a thing — and the traps that keep catching agents working in this repo.

Two audiences read this: a person adding to Hanzo, and an agent doing the same.
The rules are the same for both. The second half is written for the agent,
because every trap in it has already cost someone a wrong answer here.

## One of everything

**One base URL.** `https://api.hanzo.ai/v1`. Not a per-product host, not a
regional host, no `/api/` segment before the version. A capability is the first
segment after `/v1/`, and that segment is the whole address space: `/v1/iam`,
`/v1/flag`, `/v1/captable`.

**One key, in two classes.** There is no second credential to manage, no account
SID, no workspace id, no project header.

| Class       | Prefix | Resolves to  | Belongs                            |
| ----------- | ------ | ------------ | ---------------------------------- |
| Secret      | `sk-`  | the user     | a server you control               |
| Publishable | `pk-`  | the org only | a browser bundle, a published site |

```bash
curl -sS https://api.hanzo.ai/v1/<capability> \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

Mint either at `POST /v1/account/keys` with `{"type":"secret"}` or
`{"type":"publishable"}`. The gateway derives org, user and project from the
validated key, so a request that carries the tenant in a body field is a request
that can name somebody else's — the shape is the security property.

**One name for one thing.** Nouns are singular where the contract says so —
`/v1/experiment`, `/v1/webhook`, `/v1/template`. Check before writing a plural.

## Code

Write the smallest thing that is complete. Prefer deleting to adding, and a
value to a flag.

- **Derive, do not restate.** A second copy of a fact is a copy that will
  disagree. If a count, a list or a mapping can be computed from the contract,
  compute it.
- **Make the wrong thing unrepresentable** rather than refused. A tenant that
  cannot be named in a request needs no check that it was not.
- **Say what a thing is, not where it sits.** `quasar.RoundSigner`, not
  `LuxRoundSigner`. Namespace qualifies; the name states the value.
- **No compound word when one word does.** `clients/gateway`, not
  `clients/gatewaysvc`.
- **Comments explain WHY.** The code already says what. A comment that narrates
  the code is noise; a comment that records the measurement behind a decision is
  the most valuable line in the file.

## Code blocks

**Every fence carries a language.** A fence with an empty info string gets no
highlighting, and a reader cannot tell it from one where the highlighter simply
had nothing to say. Terminal output, ASCII diagrams and box-drawing get `text`,
which says "not code" out loud.

**One spelling per language.** `typescript`, not `ts`. `javascript`, not `js`.
`bash`, not `sh` or `cmd`. `json`, not `jsonc` unless the block really has
comments. `package-install`, not `npm`, for a dependency the reader installs.
Shiki resolves the aliases, so a mixed file is not broken — it is just two
answers to one question.

**A code sample is checked against the artifact, not remembered.** Before
writing an import, a constructor or a method name, read it out of the published
package: the tarball on npm, the wheel on PyPI, the module on the proxy, the
crate on crates.io. Every one of these pages once taught `from hanzoai import
Hanzo`, a name that distribution has never exported. The
[reference](https://docs.hanzo.ai/docs/openapi) does not have this problem because it is generated;
prose does, so prose has to go and look.

**Two forms of tab group, chosen by what is inside.** They are not
interchangeable and the choice is mechanical:

- _A code block per language, nothing else_ — put `tab="Label"` on each fence
  (`bash tab="curl"`, then `typescript tab="TypeScript"`, then `python
  tab="Python"`). Consecutive tabbed fences merge into one group, with no import
  and no JSX. A fence in the run that omits `tab` ends the group.

- _Tabs whose bodies carry prose, or nested tabs_ — the JSX form,
  `<Tabs items={[…]}>` with a `<Tab value="…">` per item. Tabs, Callout,
  Cards, Steps and the other parts are provided to every page, so a file
  imports none of them (`components/mdx.tsx` is the list).

Order the languages the same way everywhere: `curl`, CLI, TypeScript, Python,
Go, Rust, then the rest.

## Prose

Plain, specific, and short. A number beats an adjective — "381 operations" says
more than "comprehensive".

Never write a limitation you have not checked. If a guide says Hanzo cannot do
something it can, that sentence costs a customer. Before writing "no X":

1. grep the contract for the capability, and for its **singular** spelling;
2. consider the product surface, which is larger than the API surface — Hanzo
   also ships Dataroom, Console, Desktop, Team, Chat, Studio, Base, Pack;
3. only then write it, and say which of the two you checked.

## Traps

Every one of these produced a wrong answer in this repo. They are ordered by how
often they recur.

**Absence is not evidence.** An empty grep, a timed-out loop, a query against a
table with no rows in the window, a `git show` of a path that does not exist —
each returns nothing and nothing looks like proof. Echo the intermediate value
and run a control that is known to match. A scan of 28 log sources that returns
zero because it timed out reads exactly like a fleet that is idle.

**The API surface is not the product surface.** The contract describes what
`/v1` serves. It does not describe Pack's zero-config builder, the Dataroom, or
the DNS server that answers 11 routes behind a wildcard and publishes none of
them. Reading only the document produces confident claims that we lack things we
ship.

**Check the shape of an identifier before matching on it.** Image tags here are
seven characters (`sha-deda25c`), not eight. A probe built from `git log --format=%h` silently misses every one and reads as "the build failed".

**A file layout is a fact to verify, not to assume.** A Next.js export writes
`out/amqp.html`, not `out/amqp/index.html`. Checking the wrong path reports a
missing page that is present.

**In zsh, `$var:a` is a modifier.** `git show $c:apps/…` becomes
`HEADpps/apps/…`. Brace it: `${c}:apps/…`.

**`pkill -f` matches your own shell.** A pattern broad enough to catch a build
is broad enough to catch the wrapper running it, and the whole batch dies with
an exit that looks like the build failed.

**A pipe inside a table cell splits the cell**, code span or not. Reword; never
"fix" it with a regex that spans two spans and escapes the real separator.

**Generated output is not yours to commit.** A build step here rewrites over
1,500 vendored files as a side effect. Read `git status` before staging, and
stage paths, never `-A`.

**Verify the deploy, not the push.** A merged commit is not a running one. Read
the artifact — the image digest, the served `last-modified`, the running pod —
before saying a thing is live.
