Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@ The goal: make a fleet-wide change by editing **one file here**, not three repos
| `.github/actions/*` (composite) | Shared job steps. Repos call them via `uses: MustardSeedNetworks/.github/.github/actions/<name>@<sha>`. |
| `profile/README.md` | The organization profile shown at <https://github.com/MustardSeedNetworks>. Logo lives beside it. |
| `SECURITY.md`, `CODE_OF_CONDUCT.md` | Org-wide defaults GitHub applies to every repo that has no file of its own. Product repos keep their own. |
| `ui/theme/msn-shared.css` | The canonical shared interface theme. Every UI repo keeps a byte-identical copy at `ui/src/theme/msn-shared.css`; `ci-conformance` compares them by sha256. See `ui/theme/README.md`. |
| `scripts/i18n/` | The fleet i18n gate — one canonical copy of the checks; each repo keeps only its own glossary, banned vocabulary and locales. See `scripts/i18n/README.md`. |

## Discipline

Share **undifferentiated plumbing** only (dependency versions, CI gates, build
contract, release pipeline, security-crypto policy). **Never** share product
code — UI/brand tokens, product features, and authz wrappers stay per-repo,
each repo owning its own implementation.
code — product features and authz wrappers stay per-repo, each repo owning its
own implementation.

The shared interface theme is the one deliberate exception (owner, 2026-09-15).
It is undifferentiated by definition — the four products are meant to look like
one company — and keeping it per-repo is what let the copies drift into three
versions with every repo green. It is shared as a **checked file**, not as a
package: decision D6 (no shared TS package) stands, each repo still owns its
copy, and `ci-conformance` fails the copy that disagrees. The **brand** tokens
that make a product itself — `product-<name>.css` — stay per-repo.

See the remediation plan for the full rationale and the duplication scorecard.
54 changes: 53 additions & 1 deletion scripts/check-ci-conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@
merge burst silently drops main runs — and release-please, gated on
workflow_run success, then skips those commits entirely.

9. The shared interface theme matches the canonical copy byte for byte.
`ui/src/theme/msn-shared.css` had drifted into three versions across the
four UI repos with nothing able to see it, so all four shipped the same
1.45:1 light border.

Exit 0 clean, 1 on any finding.
"""

from __future__ import annotations

import hashlib
import json
import re
import subprocess
Expand Down Expand Up @@ -199,6 +205,47 @@ def linter_floor(root: Path, policy_dir: Path) -> list[str]:
return sorted(required - enabled)


def shared_theme(root: Path, repo_root: Path) -> list[str]:
"""The product's copy of the shared theme, if it has drifted.

The four UI repos each keep a copy of the colour tokens, and on 2026-09-15
they were three different files (.github#69): seed and niac-go identical,
stem with its own success green, trellis with an extra typography block.
Every repo was green throughout — a per-repo check cannot see a fleet-wide
divergence, and the divergence is what let all four ship a light-mode
border at 1.45:1 against WCAG 1.4.11's 3:1.

Compared by hash rather than by token, deliberately: a semantic comparison
would let the comments explaining WHY a value is what it is drift apart,
and those comments are the only record of the measurements behind them.

A repo with no ui/src is a backend-only repo and is not in scope.
"""
canonical = repo_root / "ui/theme/msn-shared.css"
if not canonical.exists():
return []
if not (root / "ui/src").is_dir():
return []
copy = root / "ui/src/theme/msn-shared.css"
if not copy.exists():
return [
"ui/src/theme/msn-shared.css is missing — every UI repo carries a "
"byte-identical copy of the canonical shared theme"
]
want = hashlib.sha256(canonical.read_bytes()).hexdigest()
got = hashlib.sha256(copy.read_bytes()).hexdigest()
if got == want:
return []
return [
f"ui/src/theme/msn-shared.css has drifted from the canonical theme\n"
f" found: sha256 {got[:12]}\n"
f" expected: sha256 {want[:12]} (MustardSeedNetworks/.github "
f"ui/theme/msn-shared.css)\n"
f" Change the canonical file and re-copy it into all four repos; "
f"a product-local edit cannot pass."
]


def status_vocabulary(root: Path, policy_dir: Path) -> list[str]:
"""Definitions of the fleet status vocabulary that have drifted.

Expand Down Expand Up @@ -439,7 +486,12 @@ def main() -> int:
fail(f"missing {rel} ({why})")
findings += 1

policy = Path(__file__).resolve().parent.parent / "policy"
repo_root = Path(__file__).resolve().parent.parent
for drift in shared_theme(root, repo_root):
fail(drift)
findings += 1

policy = repo_root / "policy"
if policy.exists():
try:
for missing in linter_floor(root, policy):
Expand Down
44 changes: 43 additions & 1 deletion scripts/test-check-ci-conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@


class ConformanceChecks(unittest.TestCase):
def run_checker(self, ci: str, status_definitions: str = "") -> tuple[int, str]:
def run_checker(self, ci: str, status_definitions: str = "",
theme: str | None = None) -> tuple[int, str]:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / ".github/workflows").mkdir(parents=True)
Expand All @@ -83,6 +84,9 @@ def run_checker(self, ci: str, status_definitions: str = "") -> tuple[int, str]:
if status_definitions:
(root / "ui/src").mkdir(parents=True)
(root / "ui/src/state.ts").write_text(status_definitions)
if theme is not None:
(root / "ui/src/theme").mkdir(parents=True, exist_ok=True)
(root / "ui/src/theme/msn-shared.css").write_text(theme)
p = subprocess.run([sys.executable, str(CHECKER)], cwd=root,
capture_output=True, text=True)
return p.returncode, p.stdout + p.stderr
Expand Down Expand Up @@ -195,5 +199,43 @@ def test_record_state_remains_an_alias(self) -> None:
self.assertIn("`RecordState` has drifted", out)


# ---- shared theme (.github#69) -------------------------------------
#
# The fleet shipped three different msn-shared.css files with every repo
# green, so these assert on the REAL canonical file rather than a fixture
# of one: a canonical edit that is not re-copied must fail here too.

CANONICAL = CHECKER.parent.parent / "ui/theme/msn-shared.css"

def test_identical_theme_copy_passes(self) -> None:
_, out = self.run_checker(GOOD_CI, theme=self.CANONICAL.read_text())
self.assertNotIn("has drifted from the canonical theme", out)

def test_drifted_theme_copy_is_rejected(self) -> None:
drifted = self.CANONICAL.read_text().replace(
"--color-status-success: #2a7146;", "--color-status-success: #2f7d4f;")
self.assertNotEqual(drifted, self.CANONICAL.read_text(),
"fixture did not mutate the canonical file")
code, out = self.run_checker(GOOD_CI, theme=drifted)
self.assertNotEqual(code, 0, out)
self.assertIn("has drifted from the canonical theme", out)

def test_comment_only_theme_drift_is_rejected(self) -> None:
"""Comments carry the measurements; they drift too."""
drifted = self.CANONICAL.read_text() + "\n/* product-local note */\n"
code, out = self.run_checker(GOOD_CI, theme=drifted)
self.assertNotEqual(code, 0, out)
self.assertIn("has drifted from the canonical theme", out)

def test_missing_theme_in_a_ui_repo_is_rejected(self) -> None:
code, out = self.run_checker(GOOD_CI, status_definitions="// ui repo\n")
self.assertNotEqual(code, 0, out)
self.assertIn("ui/src/theme/msn-shared.css is missing", out)

def test_backend_only_repo_is_out_of_scope(self) -> None:
_, out = self.run_checker(GOOD_CI)
self.assertNotIn("msn-shared.css", out)


if __name__ == "__main__":
unittest.main(verbosity=2)
58 changes: 58 additions & 0 deletions ui/theme/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Shared interface theme

`msn-shared.css` here is the canonical copy of the colour half of every
product's UI. Each UI repo (`seed`, `stem`, `niac-go`, `trellis`) keeps a
**byte-identical** copy at `ui/src/theme/msn-shared.css`, and the fleet
`ci-conformance` gate compares the two by sha256.

## Why a hash check

On 2026-09-15 the four copies were three different files
([.github#69](https://github.com/MustardSeedNetworks/.github/issues/69)):
`seed` and `niac-go` identical, `stem` carrying its own darker success green,
`trellis` carrying an extra typography block. Every repo was green the whole
time — a per-repo check cannot see a fleet-wide divergence — and all four
shipped a light-mode `--color-surface-border` at 1.45:1 against WCAG 1.4.11's
3:1 for a UI edge.

The comparison is by hash, not by token, on purpose: a semantic comparison
would let the comments explaining *why* a value is what it is drift apart, and
those comments are the only record of the measurements behind them.

## Changing a colour

1. Edit `ui/theme/msn-shared.css` here.
2. Copy it verbatim into all four repos' `ui/src/theme/msn-shared.css` in one
lockstep change.

A product-local edit fails `ci-conformance` by construction. A repo with no
`ui/src` directory is out of scope.

## Scope

Colour tokens plus the small component layer built on them (`.kicker`,
`.figure`, `.panel`, `.target`). A product's own type scale is **not** here —
`seed`, `stem` and `niac-go` define `.heading-1`, `.body-small` and the
`.gap-*` helpers in their own `index.css`, and `trellis` should too.

## Product hues (set C, owner 2026-09-15)

These are **not** in the shared file: each repo sets them in its own
`ui/src/theme/product-<name>.css`, because the brand anchor is the one thing
the products do not share. They are published here so the four stay one
family. Each dark value is at least 4.5:1 on the dark page ground `#181611`.

| Product | Light | Dark |
| --- | --- | --- |
| `seed` | `#2f7d3a` | `#34b244` |
| `stem` | `#1565c0` | `#2d81e1` |
| `niac` | `#6a3fa8` | `#9569d3` |
| `trellis` | `#b94689` | `#cd5199` |

## Dark mode

Dark is the night version of the light theme, not a second palette (owner,
2026-09-15). It used to sit at 195–208° cold steel against a 44–50° warm-cream
light theme and shared only the status hues. Every surface and text token is
now derived at hue 43–51°, and every pair is measured: text 4.5:1 or better,
UI edges 3:1 or better.
Loading
Loading