Skip to content

Commit efde1a9

Browse files
tallnatoclaude
andauthored
Audit remediation: deploy-time validation, deeper content checks, schema docs (#64)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 99dd254 commit efde1a9

14 files changed

Lines changed: 609 additions & 21 deletions

File tree

‎.github/PULL_REQUEST_TEMPLATE/boombox.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
**Content type:** Boombox
44

5+
See [SCHEMA.md](../../SCHEMA.md) for the full spec. Run `npm run validate` before submitting.
6+
57
### Checklist
68

79
#### Required files
810
- [ ] `<slug>.md` with frontmatter containing all required fields
9-
- [ ] Audio file: `.mp3`, `.wav`, or `.ogg`
11+
- [ ] Audio file: `.mp3`, `.wav`, or `.flac`
1012

1113
#### Frontmatter fields (in `<slug>.md`)
1214
- [ ] `title` — display name for this boombox track
1315
- [ ] `author` — creator name or handle
1416
- [ ] `description` — brief description of the track
17+
- [ ] `files` — list of `{ name, label }` for each asset (must exist in the folder)
18+
- [ ] `audio` — the audio filename (must exist)
1519
- [ ] `tags` — array of tags from `content/tags.yaml` (optional)
1620

1721
#### Naming & structure

‎.github/PULL_REQUEST_TEMPLATE/hornsound.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
**Content type:** Horn Sound
44

5+
> Custom horns play through Tesla's **Boombox** feature — WAV/MP3 only. See [SCHEMA.md](../../SCHEMA.md). Run `npm run validate` before submitting.
6+
57
### Checklist
68

79
#### Required files
810
- [ ] `<slug>.md` with frontmatter containing all required fields
9-
- [ ] Audio file: `.mp3`, `.wav`, or `.ogg`
11+
- [ ] Audio file: `.mp3` or `.wav`
1012

1113
#### Frontmatter fields (in `<slug>.md`)
1214
- [ ] `title` — display name for this horn sound
1315
- [ ] `author` — creator name or handle
1416
- [ ] `description` — brief description of the horn sound
17+
- [ ] `files` — list of `{ name, label }` for each asset (must exist in the folder)
18+
- [ ] `audio` — the audio filename (must exist)
1519
- [ ] `tags` — array of tags from `content/tags.yaml` (optional)
1620

1721
#### Naming & structure

‎.github/PULL_REQUEST_TEMPLATE/lightshow.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
**Content type:** Light Show
44

5+
See [SCHEMA.md](../../SCHEMA.md) for the full spec. Run `npm run validate` before submitting.
6+
57
### Checklist
68

79
#### Required files
810
- [ ] `<slug>.md` with frontmatter containing all required fields
9-
- [ ] `.fseq` sequence file
10-
- [ ] `.mp3` audio file (optional but recommended)
11+
- [ ] Exactly one `.fseq` sequence file
12+
- [ ] One `.mp3` audio file **sharing the `.fseq` base name** (e.g. `my-show.fseq` + `my-show.mp3`)
1113

1214
#### Frontmatter fields (in `<slug>.md`)
1315
- [ ] `title` — display name for this light show
1416
- [ ] `author` — creator name or handle
1517
- [ ] `description` — brief description of the light show
18+
- [ ] `files` — list of `{ name, label }` for each asset (must exist in the folder)
19+
- [ ] `audio` — the `.mp3` filename (must match the `.fseq` base name)
1620
- [ ] `tags` — array of tags from `content/tags.yaml` (optional)
1721

1822
#### Naming & structure

‎.github/PULL_REQUEST_TEMPLATE/locksound.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22

33
**Content type:** Lock Sound
44

5+
See [SCHEMA.md](../../SCHEMA.md) for the full spec. Run `npm run validate` before submitting.
6+
57
### Checklist
68

79
#### Required files
810
- [ ] `<slug>.md` with frontmatter containing all required fields
9-
- [ ] `.wav` audio file
11+
- [ ] One `.wav` audio file (Tesla lock chime: 24 kHz mono PCM recommended, ≤ 5 s)
1012

1113
#### Frontmatter fields (in `<slug>.md`)
1214
- [ ] `title` — display name for this lock sound
1315
- [ ] `author` — creator name or handle
1416
- [ ] `description` — brief description of the lock sound
17+
- [ ] `files` — list of `{ name, label }` for each asset (must exist in the folder)
18+
- [ ] `audio` — the `.wav` filename (must exist)
1519
- [ ] `tags` — array of tags from `content/tags.yaml` (optional)
1620

1721
#### Naming & structure
1822
- [ ] Folder placed in `content/_locksounds/<slug>/`
1923
- [ ] Slug is lowercase alphanumeric with hyphens (e.g. `r2d2-beep`)
20-
- [ ] No file exceeds 50 MB
24+
- [ ] WAV file is **≤ 1 MB**
2125

2226
### Description
2327

‎.github/PULL_REQUEST_TEMPLATE/wrap.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22

33
**Content type:** Wrap
44

5+
See [SCHEMA.md](../../SCHEMA.md) for the full spec. Run `npm run validate` before submitting.
6+
57
### Checklist
68

79
#### Required files
810
- [ ] `<slug>.md` with frontmatter containing all required fields
9-
- [ ] Image file: `.png`, `.jpg`, `.jpeg`, `.webp`, or `.svg`
11+
- [ ] Wrap image: `.png`, **512–1024 px**, ≤ 1 MB
1012

1113
#### Frontmatter fields (in `<slug>.md`)
1214
- [ ] `title` — display name for this wrap design
1315
- [ ] `author` — creator name or handle
1416
- [ ] `description` — brief description of the wrap
17+
- [ ] `files` — list of `{ name, label }` for each asset (must exist in the folder)
18+
- [ ] `thumbnail` — preview image filename (`.png/.jpg/.jpeg/.webp/.svg`, must exist)
1519
- [ ] `tags` — array of tags from `content/tags.yaml` (optional)
1620

1721
#### Naming & structure
1822
- [ ] Folder placed in `content/_wraps/<slug>/`
1923
- [ ] Slug is lowercase alphanumeric with hyphens (e.g. `matte-black`)
20-
- [ ] No file exceeds 50 MB
24+
- [ ] PNG file is **≤ 1 MB**
2125

2226
### Description
2327

‎.github/workflows/build-check.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Build Check
2+
3+
# Catches breakage in the JSON build or the Jekyll site on PRs, before it can
4+
# only surface on the production deploy (which would otherwise keep serving the
5+
# last good Pages artifact while main is broken).
6+
7+
on:
8+
pull_request:
9+
branches: [main]
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: npm
21+
22+
- run: npm ci
23+
- run: npm run build
24+
25+
- uses: ruby/setup-ruby@v1
26+
with:
27+
ruby-version: '3.3'
28+
bundler-cache: true
29+
30+
- name: Build Jekyll site (no deploy)
31+
run: bundle exec jekyll build
32+
env:
33+
JEKYLL_ENV: production

‎.github/workflows/deploy.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ jobs:
2323
with:
2424
node-version: 20
2525

26-
- run: npm install
26+
- run: npm ci
27+
28+
# Fail the deploy if any content is invalid — never ship a broken catalog.
29+
- run: npm run validate
30+
2731
- run: npm run build
2832

2933
- uses: ruby/setup-ruby@v1

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ Gemfile.lock
1717
# IDE
1818
.idea/
1919
.vscode/
20+
21+
# CodeGraph (local code-intelligence index)
22+
.codegraph/

‎.mcp.json‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"mcpServers": {
3+
"context7": {
4+
"type": "http",
5+
"url": "https://mcp.context7.com/mcp"
6+
},
7+
"codegraph": {
8+
"type": "stdio",
9+
"command": "codegraph",
10+
"args": ["serve", "--mcp"]
11+
}
12+
}
13+
}

‎CLAUDE.md‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**Volts** is the community content catalog for Tesla USB customizations — light shows, lock sounds, boombox tracks, wraps, and horn sounds. It is a **Jekyll** site (human-browsable HTML) that **also emits a JSON API** consumed by the companion **VOLTS** Kotlin Multiplatform app (`natobytes/VoltsApp`, the `app/` folder in this workspace).
8+
9+
- Repo: `natobytes/Volts`. Deployed via **GitHub Pages** at `https://natobytes.com/Volts/` (custom domain) — also reachable at `https://natobytes.github.io/Volts/`.
10+
- The app fetches **`https://natobytes.com/Volts/api/catalog.json`**. The JSON is the contract between this repo and the app — see **Data Contract** below. Don't change its shape without coordinating with `app/.../catalog/CatalogApi.kt`.
11+
- Two toolchains live side by side: **Ruby/Jekyll** renders the website; **Node/TypeScript** generates the JSON API. They are independent and only joined in CI (`cp -r public/api _site/api`).
12+
13+
## Build & Development Commands
14+
15+
```bash
16+
# One-time install (both toolchains)
17+
bundle install # Ruby/Jekyll deps (Gemfile)
18+
npm install # Node deps for the build/validate scripts (package.json)
19+
20+
# Run the website locally (http://localhost:4000/Volts/)
21+
bundle exec jekyll serve
22+
23+
# Generate the JSON API into public/api/ (catalog.json, <category>.json, tags.json)
24+
npm run build # -> ts-node scripts/build.ts
25+
26+
# Validate all content (run this before committing new content)
27+
npm run validate # -> ts-node scripts/validate.ts
28+
npm run validate -- --collection lightshows # validate just one category
29+
```
30+
31+
> `public/api/` is **gitignored** — the JSON is a build artifact, not committed. It only exists locally after `npm run build`, and in the deployed Pages artifact.
32+
33+
## Repository Structure
34+
35+
```
36+
content/ # Jekyll collections live here (collections_dir: content)
37+
_lightshows/ _locksounds/ _boombox/ _wraps/ _hornsounds/
38+
<slug>.md # item metadata (front matter) — sits BESIDE the folder
39+
<slug>/ # item's binary assets (.fseq, .mp3, .wav, .png, ...)
40+
<files>
41+
tags.yaml # canonical allowed-tags list (validation source of truth)
42+
_layouts/ # category.html, item.html, tag.html, tags.html, default.html
43+
_includes/ # nav.html, tag-filter.html
44+
_plugins/copy_collection_files.rb # copies collection binaries into _site/content/...
45+
scripts/build.ts # generates public/api/*.json (the app's feed)
46+
scripts/validate.ts # content linter (required fields, tags, file existence, sizes)
47+
<category>.md # category index pages (lightshows.md, locksounds.md, ...)
48+
tags/*.md # one page per tag (mirrors content/tags.yaml — kept in sync by hand)
49+
_data/tags.yaml # duplicate of content/tags.yaml used by templates
50+
search.json # Jekyll-rendered client-side search index for the website
51+
```
52+
53+
**Identity rule:** an item's **slug is its folder name** (`content/_<category>/<slug>/`). The metadata file must be the sibling `content/_<category>/<slug>.md` — one level **up** from the folder, not inside it. `build.ts` and `validate.ts` both key off the folder name and skip a folder with no matching `<slug>.md`.
54+
55+
## JSON Build Pipeline (`scripts/build.ts`)
56+
57+
`npm run build` reads every `content/_<category>/<slug>/` folder, parses the sibling `<slug>.md` front matter (gray-matter), and writes to `public/api/`:
58+
59+
- **`catalog.json`** — top-level `{ generatedAt, totalItems, lightshows[], locksounds[], boombox[], wraps[], hornsounds[] }`. **This is the file the app consumes.**
60+
- **`<category>.json`** — per-category `{ category, totalItems, items[] }`.
61+
- **`tags.json`** — `{ totalTags, tags: { <tag>: ["<category>/<slug>", ...] } }`.
62+
63+
Each catalog **item** is: `slug`, `category`, `title` (defaults to slug), `author` (defaults to `"Unknown"`), `description` (defaults to `""`), `tags[]`, `thumbnail` (string or `null`), `files[]`, and `meta` (the full front matter). `download` URLs are `"<baseurl>/content/<category>/<slug>/<filename>"` where `baseurl` is read from `_config.yml` (`/Volts`).
64+
65+
> ⚠️ **`files[]` is built from a disk glob of the slug folder, NOT from `meta.files`.** `build.ts` lists every file in the folder and emits `{name, downloadUrl}` for each. `meta.files[]` (with human `label`s) is carried under `meta` and separately gets `downloadUrl` injected. So the top-level `files[]` and `meta.files[]` are derived independently — a stray/extra/renamed file in the folder will appear in the feed even if it isn't declared in front matter. The app joins labels from `meta.files[]` back onto `files[]` by `name`.
66+
67+
## Data Contract (with the VOLTS app)
68+
69+
The app (`app/shared/data/.../catalog/CatalogApi.kt`) fetches `catalog.json`, reads the top-level object, and iterates a **hardcoded** list of category keys (`lightshows`, `locksounds`, `boombox`, `wraps`, `hornsounds`). Per item it reads `slug`, `category`, `title`, `author`, `description`, `tags`, `files[] {name, downloadUrl}`, `thumbnail`, `meta`, and joins per-file `label`s from `meta.files[]`.
70+
71+
Cross-repo invariants to preserve:
72+
- **Category key names** must stay exactly these five. Adding a new category here is **silently dropped** by the app until the app ships an update with the new key.
73+
- **`downloadUrl` is root-relative and includes the `/Volts` prefix** (e.g. `/Volts/content/lightshows/<slug>/<slug>.fseq`). The app resolves it against the host **only** (`https://natobytes.com`). Changing `_config.yml`'s `baseurl` silently breaks the app's URL resolution.
74+
- `meta.files[].label` is currently **required** by `validate.ts`; the app relies on it for display.
75+
76+
## Content Authoring (adding an item)
77+
78+
1. Pick the category. Create the folder `content/_<category>/<slug>/` (slug = lowercase alphanumeric + hyphens) and drop the binary asset(s) in it.
79+
2. Create the sibling `content/_<category>/<slug>.md` with front matter. Minimum required (enforced by `validate.ts`):
80+
- `title`, `author`, `description`
81+
- `files:` — a list of `{ name, label }`, each `name` must exist in the folder
82+
- `tags:` — every tag must already exist in `content/tags.yaml` (add it there first)
83+
- `audio: <filename>` — **required** for `lightshows`, `locksounds`, `boombox`, `hornsounds`
84+
- `thumbnail: <filename>` — **required** for `wraps` (must exist; png/jpg/jpeg/webp/svg)
85+
3. Allowed file extensions & size caps per category live in `scripts/validate.ts` (`ALLOWED_EXTENSIONS`, `MAX_FILE_SIZE_PER_CATEGORY`: locksounds & wraps 1 MB, else 50 MB).
86+
4. Run `npm run validate` and fix any errors. See `content/_<category>/README.md` and `.github/PULL_REQUEST_TEMPLATE/<type>.md` for per-type guidance.
87+
88+
Example (`content/_lightshows/thunderstruck-rock-show.md`):
89+
```yaml
90+
---
91+
title: Thunderstruck Rock Show
92+
author: Community (XLightShows)
93+
description: A high-energy rock-themed synchronized light show.
94+
audio: thunderstruck-rock-show.mp3
95+
files:
96+
- name: thunderstruck-rock-show.fseq
97+
label: Light Sequence
98+
- name: thunderstruck-rock-show.mp3
99+
label: Audio Track
100+
tags: [rock, synchronized]
101+
---
102+
```
103+
104+
## CI / Deployment
105+
106+
- **`.github/workflows/deploy.yml`** — on push to `main`: `npm install` → `npm run build` → `bundle exec jekyll build` → `cp -r public/api _site/api` → deploy `_site` to GitHub Pages.
107+
> ⚠️ Deploy does **not** run `npm run validate`. A direct push to `main` (or admin merge) can ship an unvalidated catalog. Validation only gates PRs.
108+
- **`.github/workflows/validate-pr.yml`** + the per-category `validate-<category>.yml` — run `npm run validate` on PRs to `main`.
109+
- **`.github/workflows/validate-single-component.yml`** — enforces that a PR touches only **one** collection.
110+
- **Renovate** (`renovate.json`) manages dependency updates.
111+
- Deployment domain (`natobytes.com`) is configured in the repo's Pages settings; there is **no committed `CNAME`** file.
112+
113+
## Conventions
114+
115+
- Commits/PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`.github/semantic.yml` enforces it). Content PRs commonly use the `content:` type/scope.
116+
- Tags are governed: the allowed set is `content/tags.yaml`. It is **duplicated** in `_data/tags.yaml` and one `tags/<tag>.md` page per tag — keep all three in sync when adding a tag.
117+
- Validation, not types, is the schema authority. There is no committed JSON Schema; `scripts/validate.ts` + the per-category README/PR templates are the de-facto spec.
118+
119+
## MCP servers
120+
121+
`.mcp.json` configures two servers for this repo:
122+
- **codegraph** — code-intelligence knowledge graph (indexed; the TS build/validate scripts and the Ruby plugin are in the graph — markdown/Jekyll templates are not parsed). Re-run `codegraph sync` after large changes.
123+
- **context7** — up-to-date library/framework docs (use for Jekyll, kramdown, gray-matter, ts-node, GitHub Actions).

0 commit comments

Comments
 (0)