Files
hanzo.ai/lib/data/cloud-primitives.ts
zeekayandHanzo Dev 2eeb7d6a4d hanzo.ai is Hanzo OS: one layer file, one product template
lib/layers.ts is the product: six layers and the Record, each with the operations
api.hanzo.ai serves for it (checked against hanzoai/cloud openapi.yaml),
a request to paste, docs and the site's own pages. /products,
/products/<layer>, the home band, the header's Product menu and the
footer row all read it.

- Header: Product, Developers, Research, Pricing, Enterprise.
- Memory leads with /v1/graph: assert, read, resolve, diff, answer.
- /sql, /kv, /docdb, /vector and the old /products/<category> addresses
  forward through one MOVED table in lib/publish.
- /affiliate and /oss state apps/affiliate's and apps/author's own
  rates; the second pages for the same programmes forward to them.
- No real-person faces on agents, no Lux or Zoo partner logos.
- Orphans linked, dead links and private-repo links removed; sitemap
  gate and nav spec hold both.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 12:23:22 -07:00

463 lines
21 KiB
TypeScript

/**
* Hanzo Cloud — the product taxonomy.
*
* WHICH products exist, what they are called, which category they sit in and in
* what order is the commerce catalog's answer, read at build time by
* `scripts/sync-catalog.mjs` into `lib/data/catalog.json`. This file turns that
* snapshot into the shape every surface renders. It holds no list of products
* and no list of categories: the previous version did, and the two answers had
* drifted far enough apart that the site sold a "Payments" category commerce
* does not carry while missing the "Dev" one it does.
*
* The split is by KIND OF FACT, not by convenience:
*
* the catalog owns membership · name · slug · order · icon · repo · kind ·
* the apiPath that proves the product exists
* this file owns the prose — a category's tagline, a leaf's blurb, the
* long-form copy a generated overview page renders, and
* the name where the catalog's belongs to another surface
*
* The catalog also states a docs page per product and this file does not read
* it: all 79 of those are `/docs/services/<slug>` and docs answers 404 for
* them. `lib/constants/products-metadata` is the one place that answers for
* docs, one verified URL at a time.
*
* A product with no prose here still renders; it reads as its name plus the
* facts the catalog states about it. Prose for a product the catalog has
* dropped renders nowhere. Neither can resurrect a product or bury one, which
* is what makes this a copy deck rather than a second taxonomy.
*
* NOTHING UNREACHABLE REACHES THIS FILE. The snapshot only carries products
* whose apiPath the served OpenAPI document answers (or, for a client, that
* this repo has a page for) — 53 of the 84 advertised, measured on every build.
* So the menu is hydrated from the catalog AND cannot advertise a 404. See
* `scripts/sync-catalog.mjs` for the gate and `scripts/audit-catalog.mjs` for
* the ratchet that stops the other 31 growing.
*
* Every leaf's `href` was resolved against the filesystem by the same sync: a
* bespoke page where this repo publishes one, `/cloud/<slug>` otherwise, and
* `cloudPrimitiveSlugs` below builds a page for every one of the latter — so a
* leaf cannot be a dead link in either branch.
*/
import type { ComponentType } from 'react'
import { policy } from '@/lib/publish'
import { opens } from '@/lib/repos'
import {
Activity, ArrowLeftRight, AudioLines, BarChart3, Bell, Blocks, Bot, Box, Boxes,
Brain, Cable, ClipboardList, Cloud, Code, Code2, Coins, Container, Cpu,
CreditCard, Database, FileText, Fingerprint, FolderGit2, FunctionSquare, Gauge,
GitBranch, Globe, Hammer, HardDrive, Image, Key, KeyRound, KeySquare, Landmark,
Layers,
LayoutDashboard, LineChart, ListChecks, Lock, MessageSquare, Monitor, Network,
NotebookPen, Package, Play, Radio, Rocket, Ruler, ScrollText, Search, Server,
Shield, ShieldCheck, Sparkles, Spline, Store, Tag, Terminal, Video, Wallet,
Waypoints, Workflow, Zap,
} from 'lucide-react'
// The attribute is not decoration: Next's bundler infers the type from the
// extension, but Node's own ESM loader refuses a JSON import without it — and
// Node is what runs the Playwright specs that import this taxonomy. Without it
// e2e/catalog-agreement.spec.ts cannot even load.
import snapshot from './catalog.json' with { type: 'json' }
export type PrimitiveStatus = 'ga' | 'beta'
export type CloudIcon = ComponentType<{ className?: string; size?: number | string }>
export interface Primitive {
/** Display name shown in the mega-menu and as the overview <h1>. */
title: string
/** Resolved link — a bespoke product page, or a generated overview route. */
href: string
/** The catalog's product slug. Also the `/cloud/<slug>` route when generated. */
slug: string
icon: CloudIcon
/** The operation that proves this product exists (`/v1/vector`). */
api?: string
/** The Google Cloud service a reader may know this by. */
gcp?: string
/** What the catalog sells this as: a service, or a client of one. */
kind?: 'service' | 'client'
/** Source — the product's repo when the catalog names one, else the org. */
github?: string
tagline?: string
description?: string
features?: string[]
status?: PrimitiveStatus
/** Short menu descriptor shown under the leaf title. */
blurb?: string
/** Resolved menu descriptor: blurb ?? tagline. */
desc?: string
}
export interface CloudCategory {
/** The catalog's category id. */
id: string
/** Category label — the mega-menu column header. */
title: string
/** Category icon — shown on the homepage overview grid. */
icon: CloudIcon
/** One-line category positioning — also the mega-menu column subtitle. */
tagline: string
/**
* The category's primitives, in the catalog's order. Depths are UNEVEN
* because they are measured, not padded to fit a grid: a category holds
* exactly the products that answer.
*/
items: Primitive[]
}
const ORG = 'https://github.com/hanzoai'
/**
* The catalog names an icon; this resolves the name to a component.
*
* Every key the catalog uses today is here, including the ones on products that
* do not currently answer — a renamed apiPath landing in commerce should light
* the product up on the next build, not draw it with a placeholder. An unknown
* name falls back rather than throwing: a marketing page missing one glyph is a
* blemish, a build that will not render is an outage.
*/
const ICON: Record<string, CloudIcon> = {
Activity, ArrowLeftRight, AudioLines, BarChart3, Bell, Blocks, Bot, Box, Boxes,
Brain, Cable, ClipboardList, Code, Code2, Coins, Container, Cpu, CreditCard,
Database, FileText, Fingerprint, FolderGit2, FunctionSquare, Gauge, GitBranch,
Globe, Hammer, HardDrive, Image, Key, KeyRound, KeySquare, Layers, LineChart,
ListChecks,
Lock, MessageSquare, Monitor, Network, NotebookPen, Package, Play, Radio,
Rocket, Ruler, ScrollText, Search, Server, Shield, ShieldCheck, Sparkles,
Spline, Store, Tag, Terminal, Video, Wallet, Waypoints, Workflow, Zap,
}
/**
* The prose for a category. The catalog carries an id, a label and an order —
* no icon and no positioning line, because those are how this site talks about
* the category rather than facts about the product line.
*/
const CATEGORY: Record<string, { icon: CloudIcon; tagline: string }> = {
ai: { icon: Brain, tagline: 'Models, agents, and inference — open weights, one API.' },
compute: { icon: Cpu, tagline: 'GPUs, machines, and functions that scale to zero.' },
data: { icon: Database, tagline: 'Every persistence primitive — at rest and in motion.' },
network: { icon: Network, tagline: 'Connect, route, and protect every service.' },
security: { icon: Shield, tagline: 'Identity, keys, and audit for the whole cloud.' },
dev: { icon: Terminal, tagline: 'Clients, SDKs, and keys for the whole API.' },
// `infrastructure`, not `platform`. The catalog renamed this category — the
// section it sits in is called Platform and a category cannot nest inside a
// section of its own name — and the key here was left on the old id, so the
// lookup missed and the category rendered with the fallback Cloud icon and NO
// tagline at all. It was the one tile with nothing written under it, on every
// surface that reads this map.
infrastructure: { icon: Layers, tagline: 'Source to production as declared state.' },
observe: { icon: Activity, tagline: 'Logs, metrics, traces, and cost in one pane.' },
web3: { icon: Landmark, tagline: 'On-chain infrastructure for the cloud.' },
apps: { icon: LayoutDashboard, tagline: 'Production apps built on the cloud.' },
}
interface Copy {
/**
* What this site calls the product, where the catalog's name is wrong for the
* page this repo publishes. It cannot add a product or bury one — the row
* still has to be in the catalog to be rendered at all — so this stays a copy
* deck. Set it only when the catalog's name belongs to a different surface.
*/
title?: string
/** Short menu descriptor. */
blurb?: string
tagline?: string
description?: string
features?: string[]
status?: PrimitiveStatus
}
/**
* What this site says about a product, keyed by the catalog's product id.
*
* Two depths, and the difference is which page the words are for. A `blurb` is
* the one line under a leaf in the menu. A `tagline` + `description` +
* `features` set is the body of a generated `/cloud/<slug>` overview, and only
* a product without a bespoke page needs one — a product with its own page
* already has all the copy it will ever have, in that page.
*/
const COPY: Record<string, Copy> = {
// --- AI --------------------------------------------------------------------
models: { blurb: 'Open-weight model garden' },
agents: { blurb: 'Build & run agents' },
embeddings: {
status: 'ga',
blurb: 'Turn anything into vectors',
tagline: 'Turn anything into vectors.',
description:
'Open embedding models for text, code, and images behind one API, wired straight into Vector for retrieval. Batch or realtime, metered per token.',
features: ['Text, code & image models', 'One API, many models', 'Pairs with Vector search', 'Token-metered billing'],
},
images: {
blurb: 'Text to image',
tagline: 'Text to image, on the path your client already calls.',
description:
'Image generation at /v1/images/generations, the OpenAI-compatible address — an existing client reaches it by changing its base URL and nothing else. Same key and same account as every other capability here.',
features: ['OpenAI-compatible address', 'One key across modalities', 'No client rewrite', 'Billed with the rest of the account'],
},
videos: {
blurb: 'Text to video',
// Video is the one modality whose shape a caller has to know about, so the
// copy leads with it rather than with the model: a render takes minutes, and
// an API that answered synchronously would be an API that timed out.
tagline: 'Text to video, as a job you can poll.',
description:
'A render takes minutes, so the call does not wait for it: POST /v1/videos/generations returns a job, GET /v1/videos/{id} reports where it has got to, and /v1/videos/{id}/content hands back the finished MP4. Nothing holds a connection open while a model works.',
features: ['Create, poll, download', 'Progress you can show a reader', 'MP4 out', 'No long-held connection'],
},
audio: {
blurb: 'Speech, music & sound',
// Named for the family and not for one verb. /v1/audio is five endpoints,
// and a product called Voice would misdescribe the four that are not.
tagline: 'Speech in, speech out, and the sound in between.',
description:
'The OpenAI-compatible pair — /v1/audio/speech for text to speech, /v1/audio/transcriptions for speech to text — beside the generative verbs the Zen family serves natively: voice, music and foley.',
features: ['OpenAI-compatible TTS & STT', 'Voice, music and foley', 'Served by the Zen family', 'One key across modalities'],
},
// --- Compute ---------------------------------------------------------------
gpus: {
status: 'ga',
blurb: 'On-demand accelerators',
tagline: 'On-demand accelerators, by the second.',
description:
'H100, A100, and consumer-class GPUs on demand — attach to a machine, a container, or a training job. Per-second billing, no reservations, no idle spend.',
features: ['H100 / A100 / L40S', 'Per-second metering', 'Attach to any workload', 'No reservation required'],
},
machines: { blurb: 'VMs by the second' },
functions: { blurb: 'Serverless handlers' },
tasks: { blurb: 'Durable scheduled work' },
// --- Data ------------------------------------------------------------------
vector: { blurb: 'Embeddings & retrieval' },
sql: { blurb: 'Relational, per tenant' },
kv: { blurb: 'Cache & queues' },
s3: { blurb: 'Object storage' },
datastore: { blurb: 'Analytical columnar store' },
base: { blurb: 'Instant app backend' },
docdb: { blurb: 'MongoDB wire protocol' },
// --- Network ---------------------------------------------------------------
gateway: { blurb: 'One API entry point' },
dns: { blurb: 'Authoritative DNS + DNSSEC' },
// --- Security --------------------------------------------------------------
iam: { blurb: 'OIDC identity & orgs' },
authz: { blurb: 'Fine-grained access' },
kms: { blurb: 'Managed keys' },
secrets: {
status: 'ga',
blurb: 'Store once, sync everywhere',
tagline: 'Store once, sync everywhere.',
description:
'Versioned secrets with per-environment scoping, synced into Kubernetes as native resources. Nothing is ever written in plaintext, and every read is audited.',
features: ['Versioned & environment-scoped', 'Kubernetes sync', 'Never stored in plaintext', 'Every read audited'],
},
audit: {
status: 'ga',
blurb: 'A tamper-evident trail',
tagline: 'A tamper-evident trail.',
description:
'Every privileged action across the cloud, recorded append-only and anchored on-chain. Query by actor, resource, or time window; export for your auditors.',
features: ['Append-only by construction', 'On-chain anchoring', 'Query by actor or resource', 'Auditor-ready export'],
},
// --- Platform --------------------------------------------------------------
projects: { blurb: 'The deploy unit' },
registry: { blurb: 'Your container registry' },
environments: {
status: 'ga',
blurb: 'Dev, staging, prod — isolated',
tagline: 'Dev, staging, prod — isolated.',
description:
'Every project gets isolated environments with their own secrets, data, and domains. Promote a build between them rather than rebuilding it.',
features: ['Isolated secrets & data', 'Per-environment domains', 'Promote, do not rebuild', 'Preview environments per branch'],
},
builds: {
status: 'ga',
blurb: 'Source to signed image',
tagline: 'Source to signed image.',
description:
'Reproducible container builds from a git push, cached and signed, pushed to your own registry. No build logic per repo — one declared pipeline.',
features: ['Build on push', 'Layer caching', 'Signed images', 'Multi-architecture'],
},
releases: {
status: 'ga',
blurb: 'Ship with confidence',
tagline: 'Ship with confidence.',
description:
'A release is an immutable pin of image plus configuration. Roll forward or back to any previous one in a single operation, with a full history of who shipped what.',
features: ['Immutable pins', 'One-step rollback', 'Full ship history', 'Progressive rollout'],
},
pipelines: {
status: 'ga',
blurb: 'CI/CD as declared state',
tagline: 'CI/CD as declared state.',
description:
'One declarative file describes build, test, and deploy for every repo in the org. Runs on your own runners; no per-repo workflow logic to keep in sync.',
features: ['One declarative file', 'Self-hosted runners', 'Shared across every repo', 'Deploy gated on tests'],
},
// --- Observe ---------------------------------------------------------------
// The catalog sells slug `o11y` as "Traces", which is the name of a different
// surface: the served document carries /v1/traces beside /v1/o11y, so an
// agent handed "use Hanzo Traces" binds to four operations instead of this
// product's 365. Every other surface for it — the h1, the footer, the route,
// the API namespace and the command — says O11y.
o11y: { title: 'O11y' },
logs: {
status: 'ga',
blurb: 'Search every line',
tagline: 'Search every line.',
description:
'Structured logs from every service in one searchable store, retained per plan and scoped per organization. No agent to install and no second log vendor.',
features: ['Structured & searchable', 'Per-organization scoping', 'No agent to install', 'Retention per plan'],
},
evals: {
status: 'beta',
blurb: 'Measure model & agent quality',
tagline: 'Measure model and agent quality.',
description:
'Benchmark, grade, and regression-test models and agents. LLM-as-judge, golden sets, and drift detection with reproducible scorecards anchored on-chain.',
features: ['LLM-as-judge & golden sets', 'Regression gates in CI', 'Drift detection', 'Anchored scorecards'],
},
metrics: { blurb: 'Time series & SLOs' },
billing: { blurb: 'Usage-based billing' },
// --- Apps ------------------------------------------------------------------
chat: { blurb: 'Conversational AI' },
bot: { blurb: 'Multi-agent platform' },
search: { blurb: 'Retrieval & answers' },
crawl: { blurb: 'Web crawler' },
}
interface Row {
id: string
name: string
category: string
slug: string
href: string
icon: string
api: string | null
docs: string | null
repo: string | null
gcp: string | null
order: number
kind?: string
}
interface Snapshot {
source: string
document: string
fetched: string
categories: { id: string; label: string; order: number }[]
products: Row[]
excluded: { id: string; category: string; api: string | null; why: string }[]
}
const catalog = snapshot as Snapshot
const toPrimitive = (row: Row): Primitive => {
const copy = COPY[row.id] ?? {}
return {
...copy,
title: copy.title ?? row.name,
href: row.href,
slug: row.slug,
icon: ICON[row.icon] ?? Boxes,
api: row.api ?? undefined,
gcp: row.gcp ?? undefined,
kind: row.kind === 'client' ? 'client' : 'service',
// The product's own repo when the catalog names one and a reader can open
// it; the org otherwise, which is never a 404.
github: row.repo && opens(row.repo) ? `https://github.com/${row.repo}` : ORG,
desc: copy.blurb ?? copy.tagline,
}
}
/**
* Every category with every leaf resolved — including withdrawn ones. The
* generated `/cloud/<slug>` pages derive from THIS set, because a withdrawn
* page still builds and answers on its URL
* (lib/publish's own contract); only its links disappear.
*/
const allCategories: CloudCategory[] = catalog.categories.map((c) => ({
id: c.id,
title: c.label,
icon: CATEGORY[c.id]?.icon ?? Cloud,
tagline: CATEGORY[c.id]?.tagline ?? '',
items: catalog.products.filter((p) => p.category === c.id).map(toPrimitive),
}))
/**
* The taxonomy SHOWS a primitive iff its page is PUBLISHED — the same
* `policy()` that writes the sitemap and the noindex tags, applied at the one
* source every display surface reads, so withdrawing a surface in lib/publish
* empties it out of all of them in the same commit. A category whose every leaf is withdrawn
* vanishes whole — an empty tile is not a tile.
*/
export const cloudCategories: CloudCategory[] = allCategories
.map((c) => ({ ...c, items: c.items.filter((item) => policy(item.href) === 'public') }))
.filter((c) => c.items.length > 0)
/**
* A small count as prose spells it — `10` → `Ten`.
*
* Display type is the one place a numeral reads worse than a word, and it is
* exactly where a hand-typed word would go stale. Beyond twelve, prose uses the
* numeral too, so the fallback is the numeral rather than a longer table.
*/
export function spell(n: number): string {
const words = [
'Zero', 'One', 'Two', 'Three', 'Four', 'Five',
'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve',
]
return words[n] ?? String(n)
}
/** Slugs hosted by the `/cloud/[slug]` dynamic route — every leaf without a page of its own. */
export const cloudPrimitiveSlugs: string[] = allCategories
.flatMap((c) => c.items)
.filter((i) => i.href.startsWith('/cloud/'))
.map((i) => i.slug)
/** Look up a primitive by slug. */
export function getPrimitive(slug: string): Primitive | undefined {
return allCategories.flatMap((c) => c.items).find((i) => i.slug === slug)
}
/** Category a primitive belongs to (for the overview breadcrumb). */
export function getPrimitiveCategory(slug: string): CloudCategory | undefined {
return allCategories.find((c) => c.items.some((i) => i.slug === slug))
}
/** Every category id the catalog carries. Each one's old `/products/<id>` address forwards to a layer (MOVED in lib/publish). */
export const categorySlugs: string[] = allCategories.map((c) => c.id)
// ── The hero tour ────────────────────────────────────────────────────────────
/** One beat of the hero's story: our line, and the API's own words for it. */
export interface Beat {
/** The line the hero types. Prose, and the only part of a beat we write. */
line: string
method: string
path: string
/** The served document's own one-line description of that operation. */
summary: string
}
/**
* The story the hero tells, resolved at build time against the SAME served
* document that decides which products exist (`scripts/sync-catalog.mjs`).
*
* A beat survives iff `/v1/openapi.json` carries its path AND its verb, so the
* hero cannot narrate an operation the platform does not serve — an invented
* path and a real path with the wrong verb are both dropped, with the reason
* printed. That is what keeps a scenario from becoming marketing fiction in a
* terminal font: the prose is ours, the verb, the path and the description are
* the API's.
*/
export const tour: { story: string; beats: Beat[] } = snapshot.tour ?? { story: '', beats: [] }