brando
brando is a module published to the tomato-bazel registry.
| Latest | 0.5.0 |
|---|---|
| Versions | 7 |
| Category | Modules & tooling |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/brando/ |
| Source | github.com/mattmarshall/brando |
bazel_dep(name = "brando", version = "0.5.0")
View source & releases on GitHub ↗
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
| rule | does |
|---|---|
brand_skin | meridian.theme.v1 textproto → binpb (schema-checked) + json |
brand_svgs | run a brand’s mark generator → layered SVGs (variants/layers own the outs) |
brand_icons | rasterize a mark → PNG / .icns / .ico |
brand_iconcomposer | Icon Composer .icon bundles |
brand_wordmark | wordmark / lockup generator (typeface) |
brand_wordmark_glyphs | ditto, for CONSTRUCTED letterforms — see //marklib:wordmark |
brand_office_pptx / brand_office_docx | branded deck / doc templates |
brand_mdbook_theme | templated mdBook theme overlay |
brand_doc / brand_latex_class | tectonic LaTeX → PDF + a templated brand class |
//marklib | shapely CSG + layered-SVG emission + Pillow rasterizer (mark-authoring lib) |
//marklib:tokens | Theme → CSS custom properties |
//marklib:palette | colour arithmetic + the WCAG contrast gate |
//marklib:diagrams | brandbook construction / grid / clearspace plates |
//fonts:space_grotesk | the 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 Brando → The 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.
| version | toolchain |
|---|---|
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.
| Atom | Resolved here | Elsewhere |
|---|---|---|
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#
Depends on
Used by (2 in the registry)
Versions#
7 published versions, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (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_check—brando.v1.Catalognames 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 declaredARTIFACT_KIND_MDBOOK_THEMEand shipped a package without one. Nothing errored —rules_brandgenerated an EMPTY:mdbook_themefilegroup, 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_themedeclares 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.shuploads viagh release uploadand 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_urlrequiresintegrity, 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.fastverkproduced seventeen artifact types andgraph/savaultproduced 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/leangresis a complete brand in a 26-line BUILD file.brand_css— Theme to CSS custom properties as a standalone artifact. The projection existed only insidebrand_mdbook_theme, so the only way to get a brand’s stylesheet was to also want an mdBook.brand_contrast_test—marklib.palettehas 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, SRIintegrity, 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_urlREQUIRESintegrity, 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/leangresand//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.bazelcannotload(), so a module cannot hand its own version to a BUILD file, which is whybrand_package’sbrando_versionsaid0.2.0whileMODULE.bazelsaid0.3.0. That field is stamped into every.brandoas provenance; a stale one is worse than an absent one.//:version_testgates it.- CI, which brando had none of: nineteen tests that only ever ran on one laptop, for a module six repos depend on.
Fixed
packedis now passed to the rasterizer as--packed. It was the last hand-sync hole in the icons path:brand_iconsDECLARED.icns/.icooutputs and nothing told the generator to emit them, so the declared and produced sets could disagree — exactly the drift the--variant/--layerflags 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.brandoarchive. One file a consumer can take instead of a pipeline: a zip holdingbrand.binpb(abrando.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 produced —
pack_brand.pyis stdlib and emits a textproto, andprotoc --encoderejects anything that does not matchbrando.v1. That keeps the protobuf wheel out of a brand repo’s graph, the propertyskin_jsonhad to be rebuilt to preserve. -
brando.v1(proto/brando/v1/brand.proto), AIP-linted viarules_aip.BrandSpecis what a human or a model authors;BrandPackageis the manifest inside an archive. It reusesmeridian.theme.v1.Themerather than redeclaring the palette contract.CatalogandArtifactKindare 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;accentis 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>.jsoneachbrand_skinalready emits, dogfoodingmarklib.tokensfor the CSS andmarklib.palettefor 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_docandbrand_latex_class. Neither had an in-repo caller — the same condition that letbrand_skinmis-resolve its schema andbrand_iconcomposership aKeyErrorfor 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_jsondid 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. ABrandSpecdoes. 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.protohas no 64-bit field, so this could not surface beforesize_bytes. marklibimported eagerly, soimport marklib.tokenspulledsvgwritedespitetokensandpalettebeing 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_schemas0.17.0 → 0.24.0.Palette.warningandPalette.infonow 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 —relsderived 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_skinresolved its own schema by accident. It emitted"@meridian//proto:theme.proto"as a plain string intonative.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 retiredmeridian0.2.3, whose 69-linetheme.protohas noTypography.display(7) and norepeated FontSource fonts(8) — so a consumer without ameridianalias failed the--encodegate 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 onmeridian_schemas, and emit every external label viaLabel(). Verified with a consumer declaring onlybazel_dep(brando).marklib.iconcomposercrashed (KeyError: 0) on the dict-form gradients 0.1.0 introduced.brand_iconcomposerhas 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 aslinear_gradient, so an.iconand its SVG cannot disagree about direction.fvslateandfvtertiarywere 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=)andbrand_latex_class(skin=)read the brand’s own<name>.jsoninstead 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 saidE0A33Ewhere its skin saidF2C46A. brand_svgs/brand_iconsown theirouts. They takevariants,layers,sizesandprefix, 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/LAYERSappeared in six BUILD files across four repos; it is gone, along with the second list.- No more
fv.fvbg/fvfg/fvaccent→brand*;--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_themeis multi-instantiable viatheme_dir; its six output paths were hardcoded, which quietly limited a package to one theme.skin_jsonreplacestextpb_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 haverules_pythonat all — but the arity and numeric facts now come fromtheme_schema.json, derived fromtheme.protoand 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;padis 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=2by 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 onborder, 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, plusbrand_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_cmdthat existed verbatim in two files. An unresolved token now fails the build;sedshipped it to the browser as a literal@ACCENT@.//fonts:space_grotesk— one copy of a face that was md5-identical in three repos, withSOURCES.jsonprovenance 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_jsondropped all but the last of a repeated field.
0.1.0
marklib: reusable N-stop / any-anglelinear_gradient(SVG + raster).
0.0.1
- Initial release — the reusable brand pipeline as a Bazel module.