From a2ee804737b16cff4dbfce3adc497ff498ce43be Mon Sep 17 00:00:00 2001 From: Ben Kearns <35475+bkearns@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:16:16 -0700 Subject: [PATCH] feat(viz): bring the memory web assets onto the published brand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit viz.html and workbench.html had drifted into a look of their own. They now use the same tokens as the site (ferrosa-docs docs/site.css), so the product and the marketing surface read as one system. ## The monogram was an invented product element Both assets rendered "Fm" with atomic number 100 — Fermium, invented for Ferrosa Memory — in the header mark and the favicon. The brand rules forbid exactly this, twice: the Fe monogram is locked to the company mark, "products do not get element tiles of their own", and under Don't, "swap element symbol or atomic number for products". Both now carry the company Fe/26 mark with the brand gradient that ends in Blued Steel, matching logo.svg. ## Tokens Names now match the site: --primary is Core Blue and --accent is Electric Cyan (the assets had --accent holding Core Blue and --accent-warm holding cyan). Invented tokens --accent-cool and --accent-purple are retired onto --primary and --steel. Blued Steel, Pale Cyan and the border/hairline scale were missing entirely and are now present. ## The graph palette extends the brand rather than inventing beside it 31 ad-hoc hues encoded entity and edge types — hot pink #ff7eb3, coral #ff6b6b, violet #c97eff, sky #87ceeb, and 20 more with no relationship to the brand. They are replaced by a categorical ramp built from the cool family (Electric Cyan -> Core Blue -> Blued Steel), with the semantic colours reserved for MEANING rather than spent on categories. Edges sit closer to steel than nodes do, so relationships recede behind the entities they connect instead of competing with them — which is also what brand guidance means by keeping Blued Steel to fills and strokes. The colours resolve from CSS tokens at render time instead of being duplicated in JS, so the graph follows the theme instead of painting dark-ground hues onto a light ground. ## Light theme Neither asset had one. Both now support an explicit data-theme and the host's preference. Light values are DARKENED counterparts rather than the dark values reused, the same way the site handles --accent and --primary; a hue picked for #05070c does not survive on #f4f8fd. Composite values became tokens for the reason the site documents: an rgba() or gradient literal cannot flip, because its alpha sits over a different ground in each theme. That covers the chrome gradients, menus, inputs, films and shadows — including shadows that were pure black, which the brand says never to use, and several neutral greys that were not brand colours at all. ## Other conformance Focus ring is 3px Electric Cyan with an offset, not a 1px hairline. All 41 border-radius declarations sit on the 4/8/12/16 scale (6px alone appeared 11 times). Pure-white selection rings and on-accent text became --text and --on-primary, which flip. ## Guard scripts/check-viz-theme.py fails on three things: the two light blocks drifting, a dark token with no light counterpart, and a colour literal painted in a rule. The first is not hypothetical — while writing this, --film was added to the explicit block and the media-query copy silently kept the old palette. Wired into `make test-unit`. The workbench branding test asserted the old state, including the Fm mark. It now asserts the brand contract and the ABSENCE of the invented tile. 109 http tests pass; ferrosa-memory-core builds; check-viz-theme clean. --- Makefile | 10 +- crates/ferrosa-memory-core/assets/viz.html | 856 +++++++++++------- .../ferrosa-memory-core/assets/workbench.html | 375 ++++++-- crates/ferrosa-memory-core/src/http.rs | 23 +- scripts/check-viz-theme.py | 108 +++ 5 files changed, 967 insertions(+), 405 deletions(-) create mode 100755 scripts/check-viz-theme.py diff --git a/Makefile b/Makefile index da56ef06..2df00236 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,17 @@ build-podman: $(PODMAN_BUILD_IMAGE) \ bash -lc '. /usr/local/cargo/env && export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get install -y --no-install-recommends cmake >/dev/null && cargo build --release -p ferrosa-memory-mcp' -test-unit: +test-unit: check-viz-theme cargo test --workspace --lib +# The two light-theme token blocks in the web assets are duplicated because CSS +# cannot alias one from the other, and duplicated blocks drift. They already did +# once, silently — a token added to the explicit block never reached the +# prefers-color-scheme copy, which only shows up on a light-preference host with +# no saved choice. +check-viz-theme: + python3 scripts/check-viz-theme.py + test-contracts: cargo test --workspace --test shared_http_deployment_spec --test expert_system_rules_spec --test expert_system_governance_spec --test tool_catalog_contract diff --git a/crates/ferrosa-memory-core/assets/viz.html b/crates/ferrosa-memory-core/assets/viz.html index 2b230a7d..b16184b6 100644 --- a/crates/ferrosa-memory-core/assets/viz.html +++ b/crates/ferrosa-memory-core/assets/viz.html @@ -4,40 +4,264 @@ Ferrosa Memory Viz - +
-
Fm100
+
Fe26
Ferrosa Memory Knowledge Graph Console diff --git a/crates/ferrosa-memory-core/src/http.rs b/crates/ferrosa-memory-core/src/http.rs index 2479a1f1..41e49ff8 100644 --- a/crates/ferrosa-memory-core/src/http.rs +++ b/crates/ferrosa-memory-core/src/http.rs @@ -6536,12 +6536,29 @@ mod tests { assert!(WORKBENCH_HTML.contains("payload.next_cursor")); assert!(WORKBENCH_HTML.contains("/tools/call")); assert!(WORKBENCH_HTML.contains("/config/tunables")); - // Memory branding swap (no terracotta accent, Fm mark) - assert!(WORKBENCH_HTML.contains("--accent: #348cff;")); + // Brand tokens carry the published names: Core Blue is --primary and + // Electric Cyan is --accent, matching the site's site.css. This used to + // assert `--accent: #348cff`, which named Core Blue as the accent. + assert!(WORKBENCH_HTML.contains("--primary: #348cff;")); + assert!(WORKBENCH_HTML.contains("--accent: #7ee7ff;")); assert!(!WORKBENCH_HTML.contains("#e2725b")); // Topbar mark uses the brand logo-icon SVG art, not a text placeholder. assert!(WORKBENCH_HTML.contains(r#"class="shell-brand-mark">Fe")); + assert!( + !WORKBENCH_HTML.contains(">Fm<"), + "the Fe monogram is company-only" + ); + assert!( + !WORKBENCH_HTML.contains(">100"), + "atomic number is 26, not a per-product one" + ); } #[test] diff --git a/scripts/check-viz-theme.py b/scripts/check-viz-theme.py new file mode 100755 index 00000000..5f8ac122 --- /dev/null +++ b/scripts/check-viz-theme.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Guard the theme token blocks in the memory web assets. + +Each asset declares its light palette twice: once for an explicit +`data-theme="light"` choice, and once inside `@media (prefers-color-scheme: +light)` for hosts that asked for light without the user picking. CSS cannot +alias one block from the other, so they are duplicated — and duplicated blocks +drift. They already did once: `--film` was added to the explicit block and the +media copy silently kept the old palette, which is invisible until someone +views the page with a light system preference and no saved choice. + +This fails loudly on three things: + 1. the two light blocks declaring different token sets or values, + 2. a token declared in dark with no light counterpart (it would keep its + dark-ground value on a light ground), + 3. colour literals painted in rules instead of tokens, which cannot flip. +""" +import re +import sys +from pathlib import Path + +ASSETS = ["crates/ferrosa-memory-core/assets/viz.html", + "crates/ferrosa-memory-core/assets/workbench.html"] + +# Tokens that are legitimately dark-only: they carry no colour. +NON_COLOUR = {"--font-sans", "--font-serif", "--font-mono", "--panel-top", + "--radius-xs", "--radius-sm", "--radius-md", "--radius-lg", + "--radius-pill"} + + +def tokens(block: str) -> dict: + return dict(re.findall(r"^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);", block, re.M)) + + +def check(path: Path) -> list: + src = path.read_text() + if "")] + style = re.sub(r"/\*.*?\*/", "", style, flags=re.S) + for line in style.split("\n"): + st = line.strip() + if st.startswith("--") or not st: + continue + if re.search(r"#[0-9a-fA-F]{3,6}\b|rgba?\(", st): + fails.append(f"{path}: literal colour in a rule (cannot flip theme) — {st[:90]}") + return fails + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + fails = [] + for rel in ASSETS: + p = root / rel + if not p.exists(): + fails.append(f"{rel}: missing") + continue + fails += check(p) + if fails: + print("check-viz-theme: FAIL") + for f in fails: + print(" ", f) + return 1 + print(f"check-viz-theme: ok ({len(ASSETS)} assets, light blocks in sync)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())