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
65 changes: 64 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,74 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Check whitespace
run: git diff --check "$(git hash-object -t tree /dev/null)" HEAD
- name: Verify foundation files

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install JavaScript development dependencies
run: npm ci

- name: Validate catalog schemas, data, and SVG assets
run: npm run validate

- name: Verify generated Cargo and npm artifacts
run: npm run generate:check

- name: Test catalog security and cross-package equality
run: npm test

- name: Check npm package types
run: npm run typecheck

- name: Verify npm package contents
run: npm pack --dry-run --workspace @stack-sh/theme

- name: Install latest stable Rust toolchain
run: rustup toolchain install stable --profile minimal --component clippy,rustfmt

- name: Check Rust formatting
run: cargo +stable fmt --check

- name: Run Rust tests
run: cargo +stable test --workspace --locked

- name: Run Clippy
run: cargo +stable clippy --workspace --all-targets --locked -- -D warnings

- name: Build Rust documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo +stable doc --workspace --no-deps --locked

- name: Verify Cargo package contents
run: cargo +stable package -p stack-theme --locked

- name: Verify repository files
run: |
test -s README.md
test -s AGENTS.md
test -s CONTRACT.md
test -s LICENSE
test -s THIRD_PARTY_LICENSES.md

msrv:
name: Minimum supported Rust
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install Rust 1.85
run: rustup toolchain install 1.85.0 --profile minimal --component clippy

- name: Run tests
run: cargo +1.85.0 test --workspace --locked

- name: Run Clippy
run: cargo +1.85.0 clippy --workspace --all-targets --locked -- -D warnings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
/target/
*.tgz
90 changes: 90 additions & 0 deletions CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Stack Theme Catalog Contract

## Status

This document defines the draft `1.0` catalog document shape. Catalog and package version `0.1.0` remain pre-1.0 and may change incompatibly before a stable release. Registry publication is outside this repository change.

The JSON Schema at [`schemas/catalog.schema.json`](./schemas/catalog.schema.json) is the machine-readable source of truth. [`catalog/catalog.json`](./catalog/catalog.json) is the only source catalog. Generated Cargo and npm copies must not be edited directly. The schema is copied into both packages; `$schema` is an editor-facing canonical repository URL and runtime consumers do not fetch it.

## Catalog identity

Every catalog has these required top-level fields:

| Field | Meaning |
| --- | --- |
| `$schema` | Schema location for editors and validators |
| `schemaVersion` | Catalog document shape; currently `1.0` |
| `catalogVersion` | Semantic version shared by the source catalog, Cargo crate, and npm package |
| `reservedThemeIds` | Previously registered theme identifiers that cannot be reused |
| `fontMetrics` | Deterministic font measurement records |
| `themes` | Themes in canonical catalog order |

Theme identifiers use the Stack language identifier form. Active identifiers are unique and cannot also be reserved. When a registered theme is removed, its identifier moves to `reservedThemeIds`; it is never reassigned.

`catalogRevision` is generated metadata rather than a source field. It is a `sha256:` digest over the normalized source catalog followed by every referenced SVG path and byte sequence in path order. The revision therefore changes when semantic catalog data, provenance, or icon bytes change, without requiring a Git commit to contain its own commit identifier.

## Theme records

A theme requires `id`, `name`, `palette`, `typography`, `nodeKindFallbacks`, `connector`, and `icons`. `description` is optional.

`palette` requires `canvas`, `surface`, `surfaceMuted`, `text`, `textMuted`, `border`, `accent`, `danger`, and `connector` colors. Colors are six- or eight-digit hexadecimal sRGB values. Other records refer to these semantic slots instead of duplicating colors.

`typography` requires:

- `fontMetricsId`, which must resolve to one catalog font metric record;
- node label, node detail, group label, and edge label sizes in integer thousandths of a CSS pixel;
- line height in integer thousandths of the selected font size;
- label and detail weights from 100 through 900.

Integer units keep Rust and JavaScript consumers from introducing representation-dependent rounding before layout begins.

`nodeKindFallbacks` requires one visual for every Stack 1.0 node kind: `actor`, `client`, `service`, `function`, `worker`, `database`, `cache`, `queue`, `storage`, and `external`. Each visual requires a renderer-supported shape, palette references, corner radius, and a theme-local `fallbackIconId`. Every fallback icon must exist in that theme.

`connector` requires line, text, and label-background palette references plus integer line width and arrow size. `dashMilliPx` is the only optional connector field; absence means a solid line.

## Font metrics

A font metric record requires `id`, `family`, `version`, `unitsPerEm`, `ascent`, `descent`, `lineGap`, `defaultAdvance`, `glyphAdvances`, and the same source, license, and distribution `provenance` required for icon assets.

Advances use integer font design units. `glyphAdvances` keys are uppercase Unicode scalar labels such as `U+0041`. A consumer uses `defaultAdvance` for a scalar without an explicit entry. This makes measurement deterministic and removes any need for a host font API. The catalog version and revision identify the exact measurement table used by a render.

## Icon metadata and provenance

Icon identifiers are unique within one theme. Each icon requires `id`, a stable logical `subject`, and `asset`; `description` is optional. When the same icon identifier appears in multiple themes, its `subject` must be identical so theme switching cannot change its meaning.

An icon asset requires a repository-relative SVG `path`, four-integer `viewBox`, and `provenance`. Provenance requires:

- HTTPS source URL and pinned source revision;
- copyright statement;
- SPDX license expression and either the root `LICENSE` or a file under `licenses/`;
- modification status;
- explicit permission for Cargo, npm, WASM, and commercial-application redistribution.

The catalog rejects assets that are not permitted in every supported distribution channel. `THIRD_PARTY_LICENSES.md` must also record third-party assets before they are committed.

SVG validation uses an element and attribute allowlist. It rejects scripts, event handlers, nested SVG documents, CDATA, processing instructions, doctypes, entities, style attributes, `href`, `url(...)`, executable schemes, data URLs, network URLs, arbitrary namespaces, and any element or attribute outside the allowlist. The root `viewBox` must exactly match catalog metadata. Renderers may inline only validated, catalog-owned SVG bytes and must never resolve a source identifier as a path or URL.

## Cargo and npm boundary

Cargo `stack-theme` exposes typed Rust records, `catalog()`, `catalog_json()`, `catalog_schema_json()`, `icon_svg()`, `CATALOG_VERSION`, and `CATALOG_REVISION`. npm `@stack-sh/theme` exposes the equivalent frozen `catalog`, `iconAssets`, `iconSvg()`, `catalogVersion`, and `catalogRevision`, plus TypeScript declarations and catalog, metadata, and schema JSON subpath exports. Referenced SVG and license files are copied into both package roots; SVG bytes are also embedded behind the Rust and JavaScript accessors so runtime consumers never resolve catalog paths through the host.

`npm run generate` validates the source catalog, checks package version equality, computes one revision, and updates both package artifacts. `npm run generate:check` fails when a generated artifact is missing or stale. Generated package data is checked into Git so Cargo and npm builds do not need network, filesystem discovery, Git, a clock, locale, or host font measurement at runtime.

The package versions and `catalogVersion` move together. Any data or asset change also changes `catalogRevision`. Consumers should record both values in render metadata; the semantic equality test verifies that the Cargo and npm artifacts contain the same catalog and metadata.

## Validation

Run the complete local gate with:

```sh
npm ci
npm run validate
npm run generate:check
npm test
npm run typecheck
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo doc --workspace --no-deps --locked
```

The complete fixture exercises every required field. Security fixtures prove rejection of script, event-handler, and external-reference SVGs. The source catalog intentionally contains no themes until the core `default`, `light`, and `dark` data lands as the next provider change.
107 changes: 107 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[workspace]
members = ["crates/stack-theme"]
resolver = "3"

[workspace.package]
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/stack-sh/theme"
rust-version = "1.85"
version = "0.1.0"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

`stack-sh/theme` is the canonical public catalog for Stack diagram themes, icons, and font metrics.

This repository currently contains only its repository foundation. Theme schemas, package APIs, and compatibility guarantees are not stable until their public contract is added in a later change.
The draft catalog contract is defined by [`CONTRACT.md`](./CONTRACT.md) and [`schemas/catalog.schema.json`](./schemas/catalog.schema.json). The canonical source is [`catalog/catalog.json`](./catalog/catalog.json); Cargo and npm artifacts are generated from that source with one content revision.

The current `0.1.0` catalog is intentionally empty. The core `default`, `light`, and `dark` themes will be added as the next reviewed catalog change before either package is released to a registry.

## Scope

Expand All @@ -19,7 +21,20 @@ Only free core themes belong in this public repository. Paid or proprietary them

## Development

Repository checks currently validate the foundation files on every push and pull request. Source-specific formatting, tests, builds, and package validation will be added together with the first catalog implementation.
Install Node.js 20 or newer and Rust 1.85, then run:

```sh
npm ci
npm run validate
npm run generate:check
npm test
npm run typecheck
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo doc --workspace --no-deps --locked
```

`npm run generate` is the only supported way to update checked-in package copies and revision metadata. CI validates the schema and asset boundary, rejects unsafe SVG fixtures, compares Cargo and npm catalog semantics, builds both packages, and verifies the minimum Rust version.

## Licensing

Expand Down
14 changes: 13 additions & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Third-party licenses

No third-party fonts, icons, code, or other assets are bundled in this repository yet.
No third-party fonts, icons, or other runtime assets are bundled in this repository yet.

The repository uses the following third-party libraries:

| Project | Version | Use | Source | Copyright | License | Distribution |
| --- | --- | --- | --- | --- | --- | --- |
| Ajv | 8.20.0 | Development-only JSON Schema validation | https://github.com/ajv-validator/ajv/tree/v8.20.0 | Evgeny Poberezkin | MIT | Installed by `npm ci`; not included in `@stack-sh/theme` |
| saxes | 6.0.0 | Development-only streaming XML parser for SVG validation | https://github.com/lddubeau/saxes/tree/v6.0.0 | Louis-Dominique Dubeau | ISC | Installed by `npm ci`; not included in `@stack-sh/theme` |
| TypeScript | 7.0.2 | Development-only npm declaration checking | https://github.com/microsoft/TypeScript/tree/v7.0.2 | Microsoft Corporation | Apache-2.0 | Installed by `npm ci`; not included in `@stack-sh/theme` |
| serde | 1.0.229 | Rust catalog deserialization and public data types | https://github.com/serde-rs/serde/tree/v1.0.229 | David Tolnay | MIT OR Apache-2.0 | Cargo dependency; no separately bundled asset |
| serde_json | 1.0.151 | Embedded JSON catalog deserialization | https://github.com/serde-rs/json/tree/v1.0.151 | David Tolnay | MIT OR Apache-2.0 | Cargo dependency; no separately bundled asset |

Ajv, saxes, and TypeScript retain their upstream licenses in installed package metadata and the npm lockfile. serde and serde_json retain their upstream Cargo package metadata and license terms. No dependency source or license text is copied into either generated distribution package by this repository.

Before adding a third-party asset, record all of the following in this file:

Expand Down
8 changes: 8 additions & 0 deletions catalog/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/stack-sh/theme/main/schemas/catalog.schema.json",
"schemaVersion": "1.0",
"catalogVersion": "0.1.0",
"reservedThemeIds": [],
"fontMetrics": [],
"themes": []
}
13 changes: 13 additions & 0 deletions crates/stack-theme/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "stack-theme"
description = "Canonical Stack theme catalog contract and embedded catalog data"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
include = ["src/**", "assets/**", "licenses/**", "schema/**", "README.md", "LICENSE"]

[dependencies]
serde.workspace = true
serde_json.workspace = true
Loading
Loading