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
77 changes: 77 additions & 0 deletions .github/check-token-names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org>
# SPDX-License-Identifier: GPL-3.0-or-later
"""Fail when a palette token name in steelbore.toml describes two colours.

Palette token names look per-palette, but every consumer that resolves a name
to a value treats them as one flat namespace. `check-palette-css.py` maps
`--void-navy` to "void navy" and walks every `[palettes.*]` table
first-writer-wins; a name bound to two hexes therefore resolves to whichever
palette appears earlier in the file, and a stylesheet declaring that token is
checked against the wrong palette's value while reporting success.

That is what happened to `Signal Green`, which named `#28C76F` in Steelbore
Blue and `#9ECE6A` in Tokyo Night from v1.39 until the v2.06 rename. It never
misfired only because no stylesheet happened to declare it. This gate makes the
next one a build failure instead of a latent one.

Shared tones are the reason this checks values rather than banning reuse
outright: `Ember Red`, `Solar Amber`, `Mint Signal` and `Ember Lift` are
deliberately carried across palettes at one value each, and that is correct.
What is never correct is one name meaning two colours.
"""

from __future__ import annotations

import pathlib
import sys
import tomllib

REPO = pathlib.Path(__file__).resolve().parent.parent
TOML = REPO / "steelbore-color-palette" / "assets" / "steelbore.toml"


def main() -> int:
try:
data = tomllib.loads(TOML.read_text(encoding="utf-8"))
except FileNotFoundError:
print(f"error: {TOML} not found", file=sys.stderr)
return 1
except tomllib.TOMLDecodeError as exc:
print(f"error: {TOML} is not valid TOML: {exc}", file=sys.stderr)
return 1

palettes = data.get("palettes")
if not palettes:
print("error: steelbore.toml has no [palettes.*] tables", file=sys.stderr)
return 1

# name -> hex -> [palette slugs]
seen: dict[str, dict[str, list[str]]] = {}
for slug, palette in palettes.items():
for key, value in palette.items():
if key == "reference" or not isinstance(value, str):
continue
seen.setdefault(key.strip().lower(), {}).setdefault(
value.upper(), []
Comment on lines +55 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize names to the CSS property namespace

When two palette tokens differ only by whitespace versus hyphens—for example, Foo Bar and Foo-Bar with different values—this checker records distinct keys and passes them, while generate-palette-css.py::kebab() maps both to --foo-bar and check-palette-css.py::kebab_to_name() treats that property as foo bar. The resulting collision is therefore exactly the ambiguity this new gate is intended to reject; normalize token names using the same kebab/space conversion before grouping them.

Useful? React with 👍 / 👎.

).append(slug)

collisions = {name: v for name, v in seen.items() if len(v) > 1}
if not collisions:
print(f"token names OK ({len(seen)} distinct names across {len(palettes)} palettes)")
return 0

for name, by_hex in sorted(collisions.items()):
print(f"`{name}` names {len(by_hex)} different colours:", file=sys.stderr)
for hexval, slugs in sorted(by_hex.items()):
print(f" {hexval} in {', '.join(sorted(slugs))}", file=sys.stderr)
print(
" A token name is a flat namespace across the whole file — rename one,\n"
" or make them the same colour if they were meant to be a shared tone.",
file=sys.stderr,
)
return 1


if __name__ == "__main__":
sys.exit(main())
192 changes: 192 additions & 0 deletions .github/generate-palette-css.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org>
# SPDX-License-Identifier: GPL-3.0-or-later
"""Regenerate the generated prologue of spacecraft.css from steelbore.toml.

`spacecraft.css` is two files in one. The prologue — the header comment, the
@font-face rules, and the `:root` custom properties — restates facts that live
in `steelbore.toml`: nine hexes, the palette's role bindings, and the Standard
version §11 was last amended at. The body below it is hand-authored layout that
references those properties through `var()` and has no business being generated.

§11.4 says values are read, never retyped. Before this script the prologue
retyped them, and the `Palette: … v1.34` line in the header had been wrong
since v1.35. `check-palette-css.py` caught a value that drifted; nothing caught
the version pin, because nothing generated it.

So: everything between the sentinels is derived, and `--check` fails the build
when the file no longer matches what the TOML implies. The body is left exactly
as found.

Regenerate with: python3 .github/generate-palette-css.py
Verify with: python3 .github/generate-palette-css.py --check

Both the canonical copy and its synced derivatives are written, so the three
stay byte-identical by construction rather than by discipline.
"""

from __future__ import annotations

import pathlib
import sys
import tomllib

REPO = pathlib.Path(__file__).resolve().parent.parent
TOML = REPO / "steelbore-color-palette" / "assets" / "steelbore.toml"

# The canonical copy first; the rest are synced derivatives (§11.4). Paths
# outside this repository are handled by their own repo's CI — the Standard's
# workflow already compares its copy against the canonical one.
TARGETS = [
REPO / "steelbore-color-palette" / "assets" / "spacecraft.css",
REPO / "spacecraft-texinfo-document" / "assets" / "spacecraft.css",
]

BEGIN = "/* >>> generated from steelbore.toml — do not edit below this line <<< */"
END = "/* >>> end generated — hand-authored layout follows <<< */"

# Theme whose tokens the stylesheet exposes. Modern is the §11.4 default and
# the palette every document uses absent a declaration.
THEME = "steelbore"

# Role tokens in the order they should appear, canvas and surfaces first.
ROLE_ORDER = [
"background",
"surface",
"surface-alt",
"foreground",
"accent",
"structure",
"success",
"error",
"warning",
"focus",
"border",
]


def kebab(name: str) -> str:
"""`Void Navy` -> `void-navy`, the inverse of check-palette-css.py's mapping."""
return "-".join(name.strip().lower().split())


def build_prologue(data: dict) -> str:
meta = data["meta"]
palette = data["palettes"][THEME]
theme = data["themes"][THEME]

# Map hex -> palette token name, so the CSS property is named after the
# token rather than after the role (a hex may serve two roles).
by_hex = {v.upper(): k for k, v in palette.items() if k != "reference"}

# Collect the distinct hexes this theme actually binds, in role order, and
# record every role each one carries so the comment is derived, not typed.
roles_for: dict[str, list[str]] = {}
order: list[str] = []
for role in ROLE_ORDER:
hexval = theme[role].upper()
if hexval not in roles_for:
order.append(hexval)
roles_for[hexval] = []
roles_for[hexval].append(role)

rows = []
for hexval in order:
token = by_hex.get(hexval)
if token is None:
raise SystemExit(
f"error: {hexval} is bound by [themes.{THEME}] but is not a named "
f"token in [palettes.{THEME}] — every role must resolve to a token"
)
rows.append((f"--{kebab(token)}:", hexval, ", ".join(roles_for[hexval]), token))

prop_w = max(len(r[0]) for r in rows)
body = "\n".join(
f" {prop:<{prop_w}} {hexval}; /* {token} — {roles} */"
for prop, hexval, roles, token in rows
)

return f"""{BEGIN}

/* Palette: {meta['standard']}. Typography: §12 (Share Tech Mono / Inconsolata,
both OFL). Values are generated from steelbore.toml — §11.4 requires they be
read, never retyped. Regenerate with .github/generate-palette-css.py.

Fonts are resolved locally and never fetched (§9.1): an installed copy is
used where one exists, and the generic monospace fallback covers the rest.
No third-party subresource is requested at render time. */

@font-face {{
font-family: 'Share Tech Mono';
src: local('Share Tech Mono'), local('ShareTechMono-Regular');
font-display: swap;
}}

@font-face {{
font-family: 'Inconsolata';
src: local('Inconsolata'), local('Inconsolata-Regular');
font-display: swap;
}}

:root {{
{body}
color-scheme: dark;
}}

{END}"""


def render(path: pathlib.Path, prologue: str) -> str:
text = path.read_text(encoding="utf-8")
head, sep, rest = text.partition(BEGIN)
if not sep:
raise SystemExit(
f"error: {path} has no generated-section sentinel.\n"
f" Expected a line reading:\n {BEGIN}\n"
" Add the sentinels around the header/@font-face/:root block first."
)
_, sep2, tail = rest.partition(END)
if not sep2:
raise SystemExit(f"error: {path} has an opening sentinel but no closing {END!r}")
return head + prologue + tail


def main() -> int:
check = "--check" in sys.argv[1:]
data = tomllib.loads(TOML.read_text(encoding="utf-8"))
prologue = build_prologue(data)

stale = []
for path in TARGETS:
if not path.exists():
print(f"error: {path} not found", file=sys.stderr)
return 1
want = render(path, prologue)
if check:
if path.read_text(encoding="utf-8") != want:
stale.append(path)
else:
path.write_text(want, encoding="utf-8")
print(f"wrote {path.relative_to(REPO)} from steelbore.toml")

if check:
if stale:
for path in stale:
print(
f"`{path.relative_to(REPO)}` is stale — it no longer matches "
"`steelbore.toml`.",
file=sys.stderr,
)
print(
"Run `python3 .github/generate-palette-css.py` and commit the result. "
"steelbore.toml is the single source (§11.4) — correct the TOML, "
"never the CSS.",
file=sys.stderr,
)
return 1
print(f"spacecraft.css is in sync with steelbore.toml ({len(TARGETS)} copies)")
return 0


if __name__ == "__main__":
sys.exit(main())
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ jobs:
- name: steelbore.scm matches steelbore.toml
run: python3 .github/generate-steelbore-scm.py --check

# Token names look per-palette but are one flat namespace to every
# consumer that resolves a name to a value — check-palette-css.py walks
# all [palettes.*] first-writer-wins, so a name bound to two hexes checks
# a stylesheet against the wrong palette and still reports success.
# `Signal Green` was exactly that from v1.39 until the v2.06 rename, and
# never misfired only because no stylesheet declared it. Shared tones
# (Ember Red, Solar Amber, Mint Signal, Ember Lift) are deliberate and
# pass — the gate gets stricter only about one name meaning two colours.
- name: Palette token names are unambiguous
run: python3 .github/check-token-names.py

# spacecraft.css's header, @font-face rules and :root block are generated
# from steelbore.toml for the same reason steelbore.scm is: they restated
# nine hexes and a §11 version pin, and the pin had been wrong since v1.35
# because nothing generated it. check-palette-css.py catches a drifted
# value; only this catches a drifted version or a reintroduced @import.
- name: spacecraft.css matches steelbore.toml
run: python3 .github/generate-palette-css.py --check

# construct-cli/ is the one real build surface in this repo — everything else
# is markdown. It went ungated until now, which is how a clippy break
# (items_after_test_module in src/sources/skillmd.rs) sat unnoticed on main:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ the rules re-attached to every prompt.
| [`spacecraft-theme-factory`](spacecraft-theme-factory/) | Generates Spacecraft Software-compliant themes for IDEs and terminals. |
| [`spacecraft-typescript-guidelines`](spacecraft-typescript-guidelines/) | Type-safe highly-concurrent TypeScript guidance (targeting TypeScript 7.0+) — Go native compiler optimizations, Project References (`composite`/`incremental`), strict type checking, non-blocking asynchronous event loops, CPU-parallel worker pools (`Piscina`), V8 engine tuning (hidden classes), and Zod data validation boundaries. |
| [`spacecraft-zig-guidelines`](spacecraft-zig-guidelines/) | Memory-safe high-performance concurrent Zig guidance — `std.Thread.Pool` / `std.Io.Threaded`, atomics, allocator discipline, comptime safety, and CPU-bound scaling patterns. |
| [`steelbore-color-palette`](steelbore-color-palette/) | Single source of truth for the Steelbore palette family (Standard §11, last amended in v2.05) — ten palettes (Modern the default, plus Classic, Blue, BlackPinkPanther, MatrixGreen, NavyWhite, Tokyo Night, Hanzo Steel, and the two §11.5 fidelity palettes Solarized Dark/Light), 19 themes with per-background WCAG matrices, the §11.1 role-token contract, the §11.1.1 accessibility variants, the §11.6 system-theme resolution and declaration contract (`[resolution]`, polarity, light/dark pairing, `SPACECRAFT_THEME`, `/etc/steelbore/theme.toml`), and the shipped `assets/steelbore.toml` + `assets/spacecraft.css`. Consumer skills defer here for color values. |
| [`steelbore-color-palette`](steelbore-color-palette/) | Single source of truth for the Steelbore palette family (Standard §11, last amended in v2.06) — ten palettes (Modern the default, plus Classic, Blue, BlackPinkPanther, MatrixGreen, NavyWhite, Tokyo Night, Hanzo Steel, and the two §11.5 fidelity palettes Solarized Dark/Light), 19 themes with per-background WCAG matrices, the §11.1 role-token contract, the §11.1.1 accessibility variants, the §11.6 system-theme resolution and declaration contract (`[resolution]`, polarity, light/dark pairing, `SPACECRAFT_THEME`, `/etc/steelbore/theme.toml`), and the shipped `assets/steelbore.toml` + `assets/spacecraft.css`. Consumer skills defer here for color values. |

<!-- §3 — Layout convention -->
## Directory layout
Expand Down
28 changes: 26 additions & 2 deletions spacecraft-steelbore-standard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ website: https://Construct.SpacecraftSoftware.org/

# The Steelbore Standard — Compliance Reference

**Version:** 2.05 | **Date:** 2026-09-15 | **Author:** Mohamed Hammad
**Version:** 2.06 | **Date:** 2026-09-15 | **Author:** Mohamed Hammad
**Maintainer:** Mohamed Hammad | **Contact:** [Mohamed.Hammad@SpacecraftSoftware.org](mailto:Mohamed.Hammad@SpacecraftSoftware.org)
**Copyright:** Copyright (C) 2026 Mohamed Hammad & Spacecraft Software | **License:** GPL-3.0-or-later
**Website:** [https://Construct.SpacecraftSoftware.org/](https://Construct.SpacecraftSoftware.org/)
Expand Down Expand Up @@ -728,6 +728,30 @@ Every Spacecraft Software application must satisfy **all three** PFA requirement
When reviewing or designing any feature that touches data handling, permissions,
or networking, verify all three PFA requirements are met.

### §9.1 — No Third-Party Subresources

The three rows above are scoped to an **application**, and that left a gap: a
document is not an application, so nothing in §9 reached the HTML this standard
publishes — which loaded its §12 fonts from a third-party CDN, disclosing every
reader's IP, User-Agent and Referer on every page view. No tracker, no analytics
SDK: the letter of the first row was met while its purpose was not.

**The rule is therefore a property of artifacts, not applications.** No
Spacecraft Software artifact — application, library, document, stylesheet,
diagram, slide, or generated page — fetches a subresource from a host the
project does not control at render time.

| Rule | Detail |
|------|--------|
| Fonts resolve locally | Baseline is `@font-face` whose `src` names `local()` only, backed by the generic `monospace` fallback. An artifact needing faithful rendering for every reader MAY also ship the file beside itself, listed after `local()` — §12's licence whitelist exists so it may be redistributed. Bundling is a fidelity choice; the fetch is what is forbidden |
| All subresource classes | Scripts, stylesheets, images and media follow the same rule. A CDN reference is third-party whatever it carries |
| Degrading is not complying | A fallback that renders acceptably when the fetch fails does not cure the fetch. The request **is** the disclosure |
| Hyperlinks are unaffected | A link the reader chooses to follow is not a subresource; this governs what an artifact loads unasked |
| Exceptions are declared | Where an artifact genuinely cannot function without a third-party fetch, document it in `README.md` naming the host, the data disclosed, and why no bundled alternative exists — the same shape as a §3.1 exemption |

A self-contained artifact is also offline-capable, reproducible, and immune to an
upstream host disappearing, so this costs little beyond the bytes it bundles.

---

## §10 — Key Bindings
Expand Down Expand Up @@ -1607,7 +1631,7 @@ Before finalising **any** Spacecraft Software artifact, mentally verify:
- [ ] **§6.5** Text files are LF-terminated, UTF-8 without BOM, and end with a newline; `.gitattributes` (`* text=auto eol=lf`) and `.editorconfig` (`charset`, `end_of_line`, `insert_final_newline`) present at the repository root; CI fails on a CR byte in a tracked text file; CRLF exceptions (vendored upstream, `cmd.exe` scripts) pinned explicitly in `.gitattributes`
- [ ] **§7** Shell scripts are POSIX-compatible; Nushell/Ion native variants provided where shell-native idioms are required; no Bashisms in shared scripts
- [ ] **§8** Texinfo manual present for user-facing programs (`doc/<project>.texi`); builds to `.info`, `.html`, and `.pdf`; `install-info` hook present in all three package manifests (§5.5) — N/A for scripts and internal tooling
- [ ] **§9** PFA: no tracking, minimal permissions, local storage default
- [ ] **§9** PFA: no tracking, minimal permissions, local storage default; **§9.1** no third-party subresources — fonts declared `local()`-first and bundled only if fidelity requires it, other assets shipped beside the artifact, nothing fetched from a host the project does not control, with any unavoidable exception declared in `README.md`
- [ ] **§10** CUA + Vim-like key bindings planned/implemented; bindings user-remappable; assistive-technology modifier chords (NVDA/Orca/VoiceOver) not captured — N/A for projects registered as games (§18.5)
- [ ] **§11** A registered palette is used — Steelbore Modern by default, or exactly one declared alternate (§11.4), never a mix; that palette's canvas is used unaltered; surface tokens are fills only, never text (§11.0.1); token-on-token pairings outside the palette's verified matrix measured before use; new apps expose colors via a named `Steelbore` theme binding the §11.1 role tokens — no bare hex literals in UI logic — and ship the palette's `-high-contrast` sibling
- [ ] **§11.6** Theme resolution implemented in two stages — base palette (in-app selection, then `SPACECRAFT_THEME`, then the §11.6.4 system declaration, then the platform color scheme, then the project's §11.4 default), then variant overlay (a pinned variant, then `NO_COLOR` ⇒ `steelbore-mono`, then §18.1 accessible mode, then platform high contrast); the registered set covers §11.6.1's fifteen eleven-role themes; an unknown or unregistered slug falls through rather than failing; palette switches are atomic and whole-surface and carry the new canvas; resolved theme and deciding source reported under `--verbose`; no dependence on per-role environment variables — Steelbore OS additionally renders `/etc/steelbore/theme.toml`, exports `SPACECRAFT_THEME`, and keeps the platform color-scheme preference in agreement with the declared polarity (§11.6.5) — N/A for artifacts with no user-facing output
Expand Down
Loading
Loading