tomato·bazeldocs v0 · latest
Docs/Reference/Modules/brando

brando

brando is a module published to the tomato-bazel registry.

Latest0.5.0
Versions7
CategoryModules & tooling
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/brando/
Sourcegithub.com/mattmarshall/brando
MODULE.bazelstarlark
bazel_dep(name = "brando", version = "0.5.0")

View source & releases on GitHub ↗

brando

brando

The reusable brand pipeline — one source pulls all the strings.


brando is a publishable Bazel module that holds the reusable brand machinery, so each brand repo supplies only its own Spec / tokens / content and shares one hermetic pipeline. Consumed by savvifi/aion//brand, fastverk/brand, tomato-bazel/brand, meridian-ux/brand and savvifi/graph//brand.

The brand catalog — every brand brando builds, rendered from the same <brand>.json each brand_skin emits, with its contrast report. One self-contained page; no external requests.

Use it

# MODULE.bazel — resolves from registry.tbzl.dev
bazel_dep(name = "brando", version = "0.2.0")
# BUILD.bazel
load("@brando//:defs.bzl", "brand_skin", "brand_svgs", "brand_icons", "brand_doc")

What it provides

ruledoes
brand_skinmeridian.theme.v1 textproto → binpb (schema-checked) + json
brand_svgsrun a brand’s mark generator → layered SVGs (variants/layers own the outs)
brand_iconsrasterize a mark → PNG / .icns / .ico
brand_iconcomposerIcon Composer .icon bundles
brand_wordmarkwordmark / lockup generator (typeface)
brand_wordmark_glyphsditto, for CONSTRUCTED letterforms — see //marklib:wordmark
brand_office_pptx / brand_office_docxbranded deck / doc templates
brand_mdbook_themetemplated mdBook theme overlay
brand_doc / brand_latex_classtectonic LaTeX → PDF + a templated brand class
//marklibshapely CSG + layered-SVG emission + Pillow rasterizer (mark-authoring lib)
//marklib:tokensTheme → CSS custom properties
//marklib:palettecolour arithmetic + the WCAG contrast gate
//marklib:diagramsbrandbook construction / grid / clearspace plates
//fonts:space_groteskthe shared OFL face (was vendored three times)

A brand writes a small gen_<mark>.py against @brando//marklib and wires the rules above; its geometry, palette, and copy stay in the brand repo.

The name

A play on Marlon BrandoThe Godfather → brand tooling that pulls all the strings: one source generates every artifact (icon, wordmark, deck, doc, theme). brando’s own mark is a vectorized Brando portrait (selfbrand/portrait/); the marionette generators in selfbrand/ remain as the marklib pipeline dogfood.

The studio

studio/ is a Next.js + eve app: a brand agency of nine agents, over the deterministic tier below it.

Why an agency rather than a prompt. StudioService is one Engine with three methods, so one model call produces a whole brand — story, palette, typography, catalog — in a single pass. Nobody staffs an agency with one generalist. Here a creative director routes to eight specialists, each owning one part of the brand and nothing else: strategist, colorist, typographer, mark_designer, wordmark_designer, platform_producer, brandbook_author, critic. The director writes no brand content of its own — the moment it starts choosing hex codes it has bypassed the contrast loop and the schema that forces it.

Nothing here computes anything. Every fact the agents produce — a contrast ratio, a stylesheet, a rendered mark — is a gRPC call into the same marklib the Bazel rules run. There is no TypeScript contrast gate, and there will not be one: a second implementation is what this repo spent three releases removing.

The colorist has a loop it cannot skip. check_contrast runs the real WCAG gate, and the instructions forbid returning a palette with any error-severity finding. That is the rule stated below — contrast is computed, never asked for — made enforceable at the point the palette is authored rather than at build time.

bazel run //service:server -- --port 50051      # the deterministic tier
cd studio && npm install && npm run dev          # the agency

studio/ is excluded from bazel build //... and has its own gate (.github/workflows/studio.yml): typecheck, tests against a real service, an eve build, and a regenerate-and-diff of the gRPC client, because a generated client nobody compares is the same unchecked declaration //tools:catalog_check exists to catch.

The service

bazel run //service:server -- --port 50051 serves five AIP-linted gRPC services: brands and their revisions, content-addressed assets, RenderBrand, and a model-assisted StudioService.

One library, two drivers. The service imports the same marklib the Bazel rules run — not a reimplementation, and not Bazel shelled out to. //service:conformance_test compares the two byte for byte across every brand in the repo, which is what keeps that from being a comment.

What it renders, and what it does not. This used to say the service could not draw a mark at all, because a spec named a generator and executing a caller-supplied generator is remote code execution rather than a feature. That refusal still stands for a MarkSpec.generator. What changed in 0.6.0 is that a spec can now carry its drawing: a brando.v1.MarkProgram is primitives, boolean operations and arithmetic over named parameters, with no assignment, no recursion and no unbounded loop, so executing one is evaluation and it terminates. //examples:citizen_sh_program_parity asserts a transcribed program emits bytes identical to the hand-written generator it replaces.

A construction that is genuinely a program — one whose shape count is not known until the CSG runs — is still not data, and generator remains the supported path for it.

The model never draws. ProposeSpec returns a BrandSpec — numbers and hex codes — and the deterministic pipeline executes them. Contrast is always computed, never asked for: a model can produce a plausible palette that fails WCAG, and asking it to check its own arithmetic is the wrong tool.

With BRANDO_MODEL_ID unset the engine is a deterministic mock, so nothing reaches Bedrock by accident. Storage is in-memory: nothing here deploys anywhere yet, and choosing a database before choosing a host would answer the harder question by accident.

Usage#

Real usage, taken from the module’s examples/.

examples/BUILD.bazel

"""The example brands, and the gate that keeps `MarkProgram` honest.

`program_parity_test.py` is shared rather than copied: each brand instantiates it
with its own `env`, the way `//service:conformance_test` is parameterised over
the brands it checks. One file means a new assertion lands for every brand at
once, which is the opposite of how a per-brand copy behaves.
"""

package(default_visibility = ["//visibility:public"])

exports_files(["program_parity_test.py"])

examples/citizen_sh/BUILD.bazel

"""citizen-sh — the second of the two brand exercises this work exists for.

Unlike leangres this one has a MARK, and that is what makes it the harder test:
an mdBook theme needs a favicon, so a brand with no mark cannot have one, and
citizen-sh wants BOTH doc surfaces — the mdBook theme for `docs/` and the LaTeX
class for the governing document its roadmap calls for. Two surfaces from one
source is the property `brand_suite` is claiming, and this is where it is checked.
"""

load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
load("//:version.bzl", "VERSION")
load("//:defs.bzl", "brand_mark_program")
load("//pkg:suite.bzl", "brand_suite")

py_binary(
    name = "gen_mark",
    srcs = ["gen_mark.py"],
    deps = ["//marklib"],
)

py_binary(
    name = "raster_mark",
    srcs = ["raster_mark.py"],
    deps = [
        ":gen_mark_lib",
        "//marklib",
        "//marklib:raster",
    ],
)

py_library(
    name = "gen_mark_lib",
    srcs = ["gen_mark.py"],
    # The rasterizer does `from gen_mark import ...`, so the module has to be
    # importable by its bare name rather than as a package path.
    imports = ["."],
    deps = ["//marklib"],
)

brand_suite(
    name = "citizen_sh",
    brando_version = VERSION,
    layers = [
        "base.svg",
        "columns.svg",
        "roof.svg",
        "svg",
    ],
    mark = ":gen_mark",
    rasterizer = ":raster_mark",
    publish_base_url = "https://github.com/mattmarshall/brando/releases/download/v0.5.0",
    publish_prefix = "",
    sizes = [
        # 256 is not decorative: `.ico` is packed from it, and render_set fails
        # loudly rather than quietly if the size it needs was never rendered.
        256,
        512,
        1024,
    ],
    skin = "citizen_sh.textpb",
    source_repo = "citizen-sh/citizen-sh",
    spec = "citizen_sh.textproto",
    variants = [
        "flat",
        "inkbg",
        "transparent",
    ],
)

# ── the same mark, as data ───────────────────────────────────────────────────
# `citizen_sh_program.textproto` is `gen_mark.py`'s geometry stated as a
# `brando.v1.MarkProgram`, and `:citizen_sh_program_parity` asserts the two emit
# byte-identical SVG.
#
# THE RULE IS INSTANTIATED, not just the test. Three bugs in this repo shipped
# for two releases apiece because a rule had no in-repo caller —
# `brand_iconcomposer` still has none — so `brand_mark_program` gets one here on
# the day it is written. The `_program` prefix keeps its filenames clear of the
# suite's; the point is that the rule runs, not that it wins the name.
brand_mark_program(
    name = "citizen_sh_program_svgs",
    layers = [
        "base.svg",
        "columns.svg",
        "roof.svg",
        "svg",
    ],
    prefix = "citizen_sh_program",
    program = "citizen_sh_program.textproto",
    skin = ":citizen_sh_skin_json",
    variants = [
        "flat",
        "inkbg",
        "transparent",
    ],
    extra_outs = [
        # Only the grounded variants have a background layer, which is the same
        # honest asymmetry `brand_svgs` carries.
        "citizen_sh_program_flat.bg.svg",
        "citizen_sh_program_inkbg.bg.svg",
    ],
)

py_test(
    name = "citizen_sh_program_parity",
    size = "small",
    srcs = ["//examples:program_parity_test.py"],
    data = [
        ":citizen_sh_program_svgs_json",
        ":citizen_sh_skin_json",
    ],
    env = {
        "BRAND": "citizen_sh",
        "PROGRAM_JSON": "$(rootpath :citizen_sh_program_svgs_json)",
        "THEME_JSON": "$(rootpath :citizen_sh_skin_json)",
    },
    main = "program_parity_test.py",
    deps = [
        ":gen_mark_lib",
        "//marklib:program",
    ],
)

examples/leangres/BUILD.bazel

"""leangres — a complete brand, in one macro call.

THIS FILE IS THE POINT OF THE WHOLE EXERCISE.

Before `brand_suite`, standing up a brand meant copying another brand's BUILD
file: `//selfbrand` is 298 lines, `fastverk/brand` more, and both carry a
hand-maintained `outs` list that has to mirror a Python dict. That is why
`fastverk` produces seventeen artifact types and `graph` and `savault` produce
two — every brand stopped wherever its author's patience did.

leangres shipped its theme, stylesheet, mdBook theme, LaTeX class and package
BEFORE it had a mark, which is the property `mark = None` exists for: a brand
should not be blocked from having a stylesheet because nobody has drawn a logo.
The mark arrived later, and adding it was two attributes.
"""

load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
load("//:defs.bzl", "brand_mark_program")
load("//pkg:suite.bzl", "brand_suite")
load("//:version.bzl", "VERSION")

py_binary(
    name = "gen_mark",
    srcs = ["gen_mark.py"],
    deps = ["//marklib"],
)

py_library(
    name = "gen_mark_lib",
    srcs = ["gen_mark.py"],
    # The rasterizer does `from gen_mark import ...`, so the module has to be
    # importable by its bare name rather than as a package path.
    imports = ["."],
    deps = ["//marklib"],
)

py_binary(
    name = "raster_mark",
    srcs = ["raster_mark.py"],
    deps = [
        ":gen_mark_lib",
        "//marklib",
        "//marklib:raster",
    ],
)

brand_suite(
    name = "leangres",
    layers = [
        "turnstile.svg",
        "halmos.svg",
        "svg",
    ],
    mark = ":gen_mark",
    rasterizer = ":raster_mark",
    sizes = [
        256,
        512,
        1024,
    ],
    variants = [
        "flat",
        "inkbg",
        "transparent",
    ],
    spec = "leangres.textproto",
    skin = "leangres.textpb",
    brando_version = VERSION,
    publish_base_url = "https://github.com/mattmarshall/brando/releases/download/v0.5.0",
    publish_prefix = "",
    source_repo = "tomato-bazel/leangres",
)


# ── the same mark, as data ───────────────────────────────────────────────────
# `leangres_program.textproto` is `gen_mark.py`'s geometry stated as a
# `brando.v1.MarkProgram`, and `:leangres_program_parity` asserts the two emit
# byte-identical SVG.
#
# THE RULE IS INSTANTIATED, not just the test. Three bugs in this repo shipped
# for two releases apiece because a rule had no in-repo caller —
# `brand_iconcomposer` still has none — so `brand_mark_program` gets one here on
# the day it is written. The `_program` prefix keeps its filenames clear of the
# suite's; the point is that the rule runs, not that it wins the name.
brand_mark_program(
    name = "leangres_program_svgs",
    layers = [
        "turnstile.svg",
        "halmos.svg",
        "svg",
    ],
    prefix = "leangres_program",
    program = "leangres_program.textproto",
    skin = ":leangres_skin_json",
    variants = [
        "flat",
        "inkbg",
        "transparent",
    ],
    extra_outs = [
        # Only the grounded variants have a background layer, which is the same
        # honest asymmetry `brand_svgs` carries.
        "leangres_program_flat.bg.svg",
        "leangres_program_inkbg.bg.svg",
    ],
)

py_test(
    name = "leangres_program_parity",
    size = "small",
    srcs = ["//examples:program_parity_test.py"],
    data = [
        ":leangres_program_svgs_json",
        ":leangres_skin_json",
    ],
    env = {
        "BRAND": "leangres",
        "PROGRAM_JSON": "$(rootpath :leangres_program_svgs_json)",
        "THEME_JSON": "$(rootpath :leangres_skin_json)",
    },
    main = "program_parity_test.py",
    deps = [
        ":gen_mark_lib",
        "//marklib:program",
    ],
)

examples/resumarsh/BUILD.bazel

"""resumarsh — a brand authored from nothing, to see what that now costs.

leangres and citizen-sh were the two exercises this work was built for. resumarsh
was not: it is a product that simply needed an identity, which makes it the
honest measure. A skin, a spec, a generator and one `brand_suite` call, with
nothing copied from another brand's BUILD file.
"""

load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("//:version.bzl", "VERSION")
load("//pkg:suite.bzl", "brand_suite")

py_binary(
    name = "gen_mark",
    srcs = ["gen_mark.py"],
    deps = ["//marklib"],
)

py_binary(
    name = "raster_mark",
    srcs = ["raster_mark.py"],
    deps = [
        ":gen_mark_lib",
        "//marklib",
        "//marklib:raster",
    ],
)

py_library(
    name = "gen_mark_lib",
    srcs = ["gen_mark.py"],
    # The rasterizer does `from gen_mark import ...`, so the module has to be
    # importable by its bare name rather than as a package path.
    imports = ["."],
    deps = ["//marklib"],
)

brand_suite(
    name = "resumarsh",
    brando_version = VERSION,
    layers = [
        "lines.svg",
        "story.svg",
        "svg",
    ],
    mark = ":gen_mark",
    rasterizer = ":raster_mark",
    publish_base_url = "https://github.com/mattmarshall/brando/releases/download/v0.5.0",
    publish_prefix = "",
    sizes = [
        # 256 is not decorative: `.ico` is packed from it, and render_set fails
        # loudly rather than quietly if the size it needs was never rendered.
        256,
        512,
        1024,
    ],
    skin = "resumarsh.textpb",
    source_repo = "resumarsh/resumarsh",
    spec = "resumarsh.textproto",
    variants = [
        "flat",
        "inkbg",
        "transparent",
    ],
)

Conformance#

1 finding across 1 invariant. 17 contested atoms. See how gating works or the full report.

D2 a non-dev register_toolchains propagates to every transitive consumer why this matters ↗
versiontoolchain
0.5.0@tectonic//:tectonic_toolchain_def

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
aspect_bazel_lib 2.22.5 2.8.1 ×1
aspect_rules_js 3.1.2 2.1.3 ×1
bazel_lib 3.2.2 3.0.0 ×12
bazel_skylib 1.8.2 1.9.0 ×2
gawk 5.3.2.bcr.3 5.3.2.bcr.1 ×12
jq.bzl 0.4.0 0.1.0 ×12
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
package_metadata 0.0.2 0.0.5 ×3
protobuf 33.4 34.0.bcr.1 ×2
rules_jvm_external 6.7 6.8 ×4
rules_nodejs 6.7.4 6.3.0 ×16.7.3 ×1
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
tar.bzl 0.10.4 0.5.1 ×110.6.0 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1
yq.bzl 0.3.4 0.1.1 ×12

Dependencies#

brando in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_python1.7.0rules_tectonic0.2.0meridian_schemas0.24.0protobuf33.4rules_proto7.1.0rules_aip0.3.0googleapis0.0.0-20260422-20ac242a

Used by (2 in the registry)

Versions#

7 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.5.0 latest 2DVdilYY7Z9+JGCc… tag archive ↗
0.4.0 ihckC6CENGvYQ2/J… tag archive ↗
0.3.0 q6uTZW3GZJRjVg4Z… tag archive ↗
0.2.0 KdV2k9/FB0GgsenY… tag archive ↗
0.1.1 e3LbEvNgyiNFQ9Rd… tag archive ↗
0.1.0 LpNCSkuX2VFPGyLf… tag archive ↗
0.0.1 shQf57exjDgyjLbP… tag archive ↗

Changelog#

0.5.0 — the Catalog is checked, and packages live on GitHub Releases

Added

  • //tools:catalog_checkbrando.v1.Catalog names the artifact kinds a complete brand includes, and nothing verified it. A declaration nothing checks is a comment that looks like data, and this one was already false: citizen-sh declared ARTIFACT_KIND_MDBOOK_THEME and shipped a package without one. Nothing errored — rules_brand generated an EMPTY :mdbook_theme filegroup, the docs would have built unstyled, and a reader would have been the first to know. The archive now depends on the gate, so a non-conformant package cannot be produced. Shipping MORE than declared stays legal: a Catalog is the floor, not the ceiling.
  • The mdBook theme travels in a package. An earlier note claimed it could not, because it is a directory. It is not — brand_mdbook_theme declares each of its six files as its own out. The wrong claim survived precisely because nothing compared the Catalog to the package.

Changed

  • Packages publish to GitHub Releases, not to a CDN bucket. The previous version pushed to aion’s S3 + CloudFront lane on the reasoning that reusing infrastructure beats building it. That weighed whether the infrastructure existed and never weighed whose it was: brando and most of the brands it packages are personal projects. Everything published there was removed.

    Mechanically: release assets are flat filenames, so prefix = "" now yields a flat key. publish_brando.sh uploads via gh release upload and gained a second guard — it already refused a plan whose sha did not match the file, and now also refuses one whose URL does not match the release being uploaded to.

    Immutability now comes from the pin, not the host. A content-addressed S3 key cannot change meaning; a release asset can be replaced. from_url requires integrity, so a swapped asset fails the fetch rather than restyling every consumer.

0.4.0 — a brand should start complete, and be publishable

Additive. Every 0.3.0 rule keeps its behavior.

Added

  • brand_suite — a brand’s whole catalog from one macro call. fastverk produced seventeen artifact types and graph/savault produced two, not because those brands wanted less but because each was wired by hand from the same rules and stopped wherever its author’s patience did. A brand now opts OUT of what it does not want rather than opting in to each thing it does. //examples/leangres is a complete brand in a 26-line BUILD file.
  • brand_css — Theme to CSS custom properties as a standalone artifact. The projection existed only inside brand_mdbook_theme, so the only way to get a brand’s stylesheet was to also want an mdBook.
  • brand_contrast_testmarklib.palette has had the WCAG checker and a waiver mechanism since 0.2.0 and nothing wrapped it, so no brand ran it in its own build. Every suite now does.
  • brand_publish_plan + tools/publish_brando.sh — distribution. The plan half is hermetic (content-addressed key, URL, SRI integrity, a paste-ready MODULE.bazel snippet), so the pin is a build artifact a test checks rather than something the publisher derives by hand. rules_brand.from_url REQUIRES integrity, and computing an SRI hash manually is the step people skip — which matters, because an unpinned brand means a swapped CDN object restyles every consumer at once, silently. The upload half refuses a plan whose sha does not match its file.
  • //examples/leangres and //examples/citizen_sh — the two brand exercises this work exists for, worked end to end. citizen-sh is the two-surface test: the LaTeX class for its governing document and the stylesheet plus favicon for its docs, from one package.
  • //:version.bzl — one copy of the version. MODULE.bazel cannot load(), so a module cannot hand its own version to a BUILD file, which is why brand_package’s brando_version said 0.2.0 while MODULE.bazel said 0.3.0. That field is stamped into every .brando as provenance; a stale one is worse than an absent one. //:version_test gates it.
  • CI, which brando had none of: nineteen tests that only ever ran on one laptop, for a module six repos depend on.

Fixed

  • packed is now passed to the rasterizer as --packed. It was the last hand-sync hole in the icons path: brand_icons DECLARED .icns/.ico outputs and nothing told the generator to emit them, so the declared and produced sets could disagree — exactly the drift the --variant/--layer flags removed everywhere else.

0.3.0 — a brand becomes a package

Additive. Every 0.2.0 rule keeps its behavior; the new surface is brand_package and the brando.v1 protos it encodes.

Added

  • brand_package — the .brando archive. One file a consumer can take instead of a pipeline: a zip holding brand.binpb (a brando.v1.BrandPackage), brand.json (the same manifest, for Starlark) and every artifact under a content-addressed path. Identical bytes land once however many logical names point at them — a brand’s flat and mono marks are routinely the same file — and a consumer asks for the name, never the hash. Byte-reproducible, with a test.

    It replaces two hand-assembled asset zips (aion-brand-assets.zip, tbzl-brand-assets.zip) with two different layouts, two naming conventions and no manifest in either. Four of the six brands had no bundle at all. A zip with no manifest is a bag of files: you can extract it, but you cannot ask it for “the favicon” without already knowing what this brand called that.

    The manifest is protoc-validated, not merely producedpack_brand.py is stdlib and emits a textproto, and protoc --encode rejects anything that does not match brando.v1. That keeps the protobuf wheel out of a brand repo’s graph, the property skin_json had to be rebuilt to preserve.

  • brando.v1 (proto/brando/v1/brand.proto), AIP-linted via rules_aip. BrandSpec is what a human or a model authors; BrandPackage is the manifest inside an archive. It reuses meridian.theme.v1.Theme rather than redeclaring the palette contract. Catalog and ArtifactKind are the formalization: “what does a complete brand include” stops being “whatever fastverk happened to wire up”.

  • brando has a skin (//skins:brando). It shipped a mark, a wordmark and an entire brand pipeline for three releases with no skin of its own, which is a poor advertisement for a tool whose argument is that one source drives everything. Cream on ink; accent is a value step rather than a hue, because the identity is monochrome on purpose. Zero unreadable pairs under the gate.

  • //console — a browsable catalog rendering every brand from the <brand>.json each brand_skin already emits, dogfooding marklib.tokens for the CSS and marklib.palette for the contrast report. Each card wears its own skin via scoped custom properties, so the brands read as different brands rather than six identically-styled swatch grids.

  • Fixtures for brand_doc and brand_latex_class. Neither had an in-repo caller — the same condition that let brand_skin mis-resolve its schema and brand_iconcomposer ship a KeyError for two releases. There is now one fixture per emitted file: the article class and the beamer theme, because 0.2.0’s rename broke a call site the single article fixture never touched.

Fixed

  • skin_json did not implement string concatenation. Adjacent string literals join, as in C — how every textproto wraps prose. Neither this parser nor the tokenizer it replaced handled it, and nothing noticed, because no skin has a field long enough to wrap. A BrandSpec does. The failure is badly localized: the second literal reads as a scalar with no field name, so the error names a position several lines past the real one.
  • Enums could not be parsed. A textproto writes an enum as a bare identifier, which lexes exactly like a field name; only the schema can distinguish them.
  • int64 is now its own schema kind. It is the one type whose two encodings genuinely disagree: bare in a textproto, a string in proto3-JSON, because JSON numbers cannot carry the full int64 range. theme.proto has no 64-bit field, so this could not surface before size_bytes.
  • marklib imported eagerly, so import marklib.tokens pulled svgwrite despite tokens and palette being stdlib-only by design. The tests never caught it — they run under Bazel, where every wheel is present. Now lazy (PEP 562), asserted in a subprocess.

Changed

  • meridian_schemas 0.17.0 → 0.24.0. Palette.warning and Palette.info now encode for the first time in the fleet. Zero of six skins set them, and that was not neglect: no skin could. The fields arrive in 0.20.0, and 0.20.0–0.24.0 were tagged upstream but never published — rels derived the registry directory from the repo basename rather than the module’s declared name, so five releases wrote to a path Bazel never reads.

0.2.0 — the consolidation release

Breaking for brand_mdbook_theme and brand_latex_class; every other rule keeps a working legacy path so a brand can adopt 0.2.0 in one commit and migrate its generators in another.

Fixed — bugs that were shipping

  • brand_skin resolved its own schema by accident. It emitted "@meridian//proto:theme.proto" as a plain string into native.genrule(srcs=), and bzlmod resolves a plain-string label in a legacy macro with the calling package’s repo mapping. Which schema validated a skin was therefore an accident of the consumer’s dependency graph. brando pinned the retired meridian 0.2.3, whose 69-line theme.proto has no Typography.display (7) and no repeated FontSource fonts (8) — so a consumer without a meridian alias failed the --encode gate on exactly the fields a brand needs to ship its own faces. aion had forked these genrules into its own BUILD file to work around it. Fixed in both halves: depend on meridian_schemas, and emit every external label via Label(). Verified with a consumer declaring only bazel_dep(brando).
  • marklib.iconcomposer crashed (KeyError: 0) on the dict-form gradients 0.1.0 introduced. brand_iconcomposer has no caller anywhere in the fleet, which is why it shipped broken for two releases. The gradient angle is now honoured rather than dropped, using the same vector construction as linear_gradient, so an .icon and its SVG cannot disagree about direction.
  • fvslate and fvtertiary were the same token under two names — the LaTeX class called it tertiary, the beamer theme called it slate. Unified.

Changed

  • Palettes come from the skin. brand_mdbook_theme(skin=) and brand_latex_class(skin=) read the brand’s own <name>.json instead of taking re-typed colour attributes. A palette was previously authored two to five times per brand, and had already drifted: fastverk’s LaTeX accent said E0A33E where its skin said F2C46A.
  • brand_svgs / brand_icons own their outs. They take variants, layers, sizes and prefix, compute the outputs, and pass the same lists to the generator. Bazel checks declared ⊆ produced but not the reverse, so a layer added to a generator and not to the BUILD list was written and silently dropped. The comment # keep in sync with gen_mark.py's VARIANTS/LAYERS appeared in six BUILD files across four repos; it is gone, along with the second list.
  • No more fv. fvbg/fvfg/fvaccentbrand*; --fv-*--brand-*; .fv-cta.brand-cta. Template tokens moved from @INK@/@CREAM@/ @TERTIARY@ to the schema’s role names, and the CSS variables with them.
  • brand_mdbook_theme is multi-instantiable via theme_dir; its six output paths were hardcoded, which quietly limited a package to one theme.
  • skin_json replaces textpb_to_json. Same stdlib-only property — that is a hard requirement, since it runs in the consumer’s build and a brand repo need not have rules_python at all — but the arity and numeric facts now come from theme_schema.json, derived from theme.proto and diffed by //skins:theme_schema_test, rather than from two hand-written sets. Output is byte-identical to the tokenizer it replaces.

Added

  • //marklib:fit — one model→pixel transform. Four brands had four incompatible formulas; pad is now documented (a fraction of the full canvas, per side) and a test pins that it reproduces aion’s existing values exactly.
  • //marklib:raster.render_set — the rasterizer driver, ~280 lines of near-identical copy across five repos. ss=2 by default, so fastverk and meridian pick up the antialiasing they silently lacked. A test asserts it is byte-identical to the hand-rolled loop.
  • //marklib:tokens — Theme → CSS custom properties, previously implemented three times with three fallback tables.
  • //marklib:palette — WCAG contrast, with severity tiers. Run flat it flagged all six skins on border, which is a bad rule rather than six bad palettes; only pairs whose usage is unambiguous fail a build.
  • //marklib:diagrams — brandbook construction / grid / clearspace, lifted from fastverk. The reference circle is measured through the caller’s transform rather than an assumed scale, which is what made the original brand-specific.
  • //marklib:wordmark — placement, box fitting and lockup composition, plus brand_wordmark_glyphs, for a brand whose letterforms are constructed rather than set in a typeface. aion hand-rolled 143 lines for want of this.
  • //tools:render_template — one templating path, replacing a _sed_cmd that existed verbatim in two files. An unresolved token now fails the build; sed shipped it to the browser as a literal @ACCENT@.
  • //fonts:space_grotesk — one copy of a face that was md5-identical in three repos, with SOURCES.json provenance and the OFL inside the filegroup.

Tests

1 → 14. brando previously tested only its textproto converter. Notably //marklib:gradient_parity_test: linear_gradient ships twice, once for SVG and once for raster, and nothing compared them — a divergence there does not crash or fail a build, the app icon’s gradient simply runs the other way from the website’s. //skins:fixture makes brando dogfood brand_skin, and //doc:fixture_class / //mdbook:fixture_theme do the same for the rules that had no in-repo caller — the condition under which all three of the bugs above shipped.

0.1.1

  • skins: textpb_to_json dropped all but the last of a repeated field.

0.1.0

  • marklib: reusable N-stop / any-angle linear_gradient (SVG + raster).

0.0.1

  • Initial release — the reusable brand pipeline as a Bazel module.

← All modules