Files
zeekayandHanzo Dev 17cfa2a56f CI answers again: attestations is served, and Even's rules are the kit's own stylesheet
The cloud now serves attestations, so its absent exemption goes, as the audit
asks. Even wrote its rules as a joined string the Tailwind floor cannot read,
so bench-even counted as a utility; written as the inline stylesheet the floor
reads, the same rules put the count back at 0.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-27 16:52:46 -07:00

140 lines
6.4 KiB
JavaScript

/**
* The commerce catalog, and whether api.hanzo.ai serves what it advertises.
*
* ONE probe, two callers, because there is one question underneath both:
* does this product exist? `audit-catalog.mjs` asks it to GATE the build on new
* drift; `sync-catalog.mjs` asks it to decide what the site renders. Writing the
* predicate twice is how the gate and the menu come to disagree — the gate says
* a product is missing while the menu still advertises it, which is the exact
* shape of every drift this repo has already paid for.
*
* The authority is the SERVED DOCUMENT, not an HTTP probe of each path.
* `/v1/openapi.json` is a projection of the routers cloud mounted, so a path in
* it exists by construction and a path absent from it answers 404. Reading it
* once is also the only form of this measurement that needs no credential, hits
* no rate limit, and gives the same answer from any network — an HTTP probe
* returns 401/403 for a path that exists and 404 for one that does not, which
* is the same verdict obtained 84 times over with auth in the way.
*/
export const API = process.env.CATALOG_API ?? "https://api.hanzo.ai";
export const CATALOG_URL = `${API}/v1/commerce/catalog?brand=hanzo`;
export const DOCUMENT_URL = `${API}/v1/openapi.json`;
/**
* Every product whose `apiPath` is known not to answer, and WHY. Four reasons,
* and only one of them is a hole in the cloud:
*
* renamed — the capability is served, the apiPath is just the wrong spelling.
* `served` names the path that does answer. These are pure catalog
* bugs and the cheapest thing on this list to clear.
* client — a CLI, SDK, IDE, desktop or web client. It consumes the API and
* is not an operation, so no apiPath can ever be right. These want
* a different field, not a different path.
* external — served by another network (Lux), not by api.hanzo.ai.
* absent — ADVERTISED AND NOT BUILT. This is the only bucket that is a
* commercial problem, and the only one a code change here cannot
* fix. Clearing it means shipping the product.
*
* The audit holds this as a RATCHET that may only shrink. The sync reads it for
* one thing only — the word it prints beside an excluded product — so an
* exclusion is always reported with its reason rather than as a bare count.
*/
export const KNOWN_UNSERVED = {
// --- renamed: the capability exists, the apiPath does not ------------------
//
// Only the two the SINGULAR RULE below cannot derive. This list used to hold
// twelve, and four of them had gone stale pointing at paths that no longer
// answer either — prompts at /v1/agents, referrals at /v1/commerce/referrals
// — which is what a hand-kept table of other people's renames does.
containers: { why: "renamed", served: "/v1/platform/projects" },
research: { why: "renamed", served: "/v1/search" },
overview: { why: "absent", note: "/v1/admin/overview stopped answering; nothing serves the operator overview" },
// --- client: consumes the API, is not an operation -------------------------
cli: { why: "client" },
sdks: { why: "client" },
desktop: { why: "client" },
console: { why: "client" },
studio: { why: "client" },
api: { why: "client", note: "the API itself — /v1/api was never a route" },
// --- absent: advertised, enabled, and nothing serves it ---------------------
edge: { why: "absent", note: "hanzoai/edge is real Rust, never mounted into cloud" },
cdn: { why: "absent", note: "hanzoai/cdn is our own asset bucket, not a tenant product" },
hsm: { why: "absent", note: "hanzoai/hsm is a Go library behind /v1/kms, not a surface" },
mpc: { why: "absent" },
vpc: { why: "absent" },
"load-balancer": { why: "absent" },
safe: { why: "absent" },
governance: { why: "absent" },
daowork: { why: "absent" },
};
export const REASONS = ["renamed", "client", "external", "absent"];
/**
* A product answers if the document carries its apiPath exactly, or carries any
* path beneath it. The prefix case is not a loophole: `/v1/wallets` with only
* `/v1/wallets/{id}` served still means a reader following that product reaches
* a real operation, which is the whole claim an apiPath makes.
*/
/**
* The same path with its family made singular, or the path unchanged.
*
* THE API SINGULARISED ITS SEGMENTS and the commerce catalog did not follow:
* /v1/agents is /v1/agent, /v1/evals/datasets is /v1/eval/datasets, /v1/bots is
* /v1/bot. Sixteen of the seventeen products whose advertised path now answers
* 404 answer at the singular, which makes it a rule rather than sixteen rows
* somebody maintains — and the rows it replaces were already wrong.
*
* Only the FIRST segment moves, because that is the family and the family is
* what was renamed; /v1/eval/datasets keeps its plural tail. Nothing here
* decides anything on its own: the candidate goes through `serves` exactly like
* the advertised path, so a word that merely ends in "s" costs nothing.
*/
export const singular = (apiPath) =>
String(apiPath ?? "").replace(/^(\/v1\/[a-z0-9-]*[a-z])s(\/|$)/, "$1$2");
/**
* The path that ANSWERS for a product, or null: the one advertised, else the
* singular of it, else the rename this file records. Every candidate is tested;
* none is believed on its shape.
*/
export const answers = (paths, apiPath, id) => {
for (const p of [apiPath, singular(apiPath), KNOWN_UNSERVED[id]?.served]) {
if (p && serves(paths, p)) return p;
}
return null;
};
export const serves = (paths, apiPath) => {
if (!apiPath) return null;
if (paths.has(apiPath)) return "exact";
const under = apiPath.replace(/\/$/, "") + "/";
for (const p of paths) if (p.startsWith(under)) return "prefix";
return null;
};
const get = async (url) => {
const res = await fetch(url, { headers: { accept: "application/json" } });
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
return res.json();
};
/**
* Both documents, or the reason neither arrived.
*
* Returns `{ error }` rather than throwing: an unreachable API is a fact each
* caller answers differently — the audit declines to judge, the sync keeps the
* committed snapshot — and neither of those is an exception.
*/
export async function read() {
try {
const [catalog, document] = await Promise.all([get(CATALOG_URL), get(DOCUMENT_URL)]);
return { catalog, document, paths: new Set(Object.keys(document?.paths ?? {})) };
} catch (e) {
return { error: e.message };
}
}