After a hosting deploy of the composites app, fetches the changed files off feb-composites.web.app and checks they match the commit.

I put this together in July 2026, after comp, from everything we did in SN5 — the Drive, the full #composites and #purchasing history, and the manufacturer datasheets for the stuff we actually buy. It's the handoff I wish I'd gotten: what went wrong and why, standards so the answers stop living in people's heads, and a work-order system so we can actually trace what we built. Everything went through staged reviews (G0–G4) against a reviewer agent loaded with our constraints and SN5 history before it landed here.
— Simon
New lead? Start with HANDOFF.md. It explains how to run and care for everything in here.
| Folder | Contents | Start with |
|---|---|---|
00 Agent/ | The "simon" reviewer-agent definition. Archival copy; the live one is at composites_programs/.claude/agents/simon.md | |
01 Pain Points and Improvements/ | The SN5 season review: what went well, 10 major problems with root-cause analyses, traceability to the fixes | the .docx |
02 CS Standards/ | 14 numbered composites standards (CS-000 to CS-013). The markdown in src/ is the canonical text; the .docx files are built output. Figures are SVG in src/figures/ with rendered PNGs beside them | CS-INDEX |
03 Datasheets/ | 25 manufacturer TDS/SDS PDFs for the products we actually use | INDEX.md |
04 Printables/ | Shop reference sheets meant to be printed: resin ratios, flowcharts, checklists | README.md |
05 Design System/ | The app's visual language as a reusable system: tokens, component CSS, a living style guide | styleguide.html |
06 Composites App/ | The composites work-order app, live at feb-composites.web.app | app/README.md |
07 CFD PDF Viewer/ | The desktop original of the CFD viewer, kept buildable; the hosted version lives in 08 | README.md |
08 CFD Sims Dashboard/ | The CFD app, live at feb-cfd.web.app on its own Firebase project: a dashboard of every report with its downforce and drag by design point, saved views, and the report viewer from 07, all in the composites app's shell | README.md |
09 Website/ | The public team website, built on the design system. Not deployed; its README has the state of it | README.md |
10 Fusion Add-in/ | FEBPlanStock/, a Fusion add-in that runs the stack planner from inside Fusion and draws the board layers over the mold, plus the feasibility study and spikes that led to it. Install it from the latest release: download the zip, unzip, double-click the installer | 10 Fusion Add-in/README.md |
tools/ | Everything that builds and checks the rest: the docx builder, the generators, the servers, and 24 test suites | README.md |
SETUP.md is the full walkthrough for a new machine, macOS or Windows: what to install, how to verify it, how to run the suite, how to deploy, and the platform traps that cost a day each if you meet them cold.
The short version. You need Node for the app tooling, Playwright for the browser tests, the Firebase CLI and a JDK for the rules tests and deploys, and Python 3 only if you are editing the standards. Three commands cover most days, all run from this folder:
node tools/serve_populated.mjs --port 8791 # the app locally, seeded, no Firebase
node tools/test_app.mjs # the core logic suite
node tools/test_designsystem.mjs # CSS drift check, ~1 second
The live app needs no setup at all: it is at https://feb-composites.web.app, and access is controlled by the roster inside it.
The repo was renamed from feb-composites-applications to feb-engineering-apps on 2026-09-02, when the CFD dashboard joined it. The old URL redirects. The folders were renumbered the same day: reference material first, the shared design system, then the four apps together, with 03 App/ becoming 06 Composites App/. The two apps are separate Firebase projects on purpose: feb-composites and feb-cfd, each deployed from its own folder, so a deploy of one can never touch the other's rules or data.
Anyone can open it and press View as guest: the whole app, read-only, with no account and nothing to ask a lead for. Editing needs a name, because every buy-off carries one.
06 Composites App/app/ is the team's shared workspace for a season, running on Firebase. Sign-up is self-serve: a name, a username and a password get you in as a member. It updates live for everyone and works on phones and tablets as well as desktop. The full manual lives in 06 Composites App/app/README.md and the architecture in 06 Composites App/app/DESIGN-NOTES.md; this is the short tour.

Twelve tabs, grouped in the sidebar by who is asking:


Cross-links are everywhere; click a chip to jump to the related record. ⌘K searches everything. Light and dark themes follow the system setting, and printing always comes out black-on-white. Access is enforced server-side by firestore.rules. Signing up creates your own roster entry as a member and nothing more: you cannot name your own role, and only a lead can grant one or remove anybody.
Every physical thing gets a 4 × 1 inch label: the ID, the fact that actually identifies it, and a QR code. A plain phone camera opens a public nameplate saying what the object is, its stage and where it lives, no account and no install; names, costs and files stay behind the roster. The in-app Scan button makes a move two scans (the object, then the shelf), works on iPhones through a lazy-loaded wasm decoder, and reads the UC EH&S barcode tags on chemical containers, so the campus sticker is the container's identity. The cure buy-off captures which fabric roll and which resin and hardener lots went in, and "I don't know" is a recorded answer. The bulk builder prints Avery sheets with a 100 mm calibration bar, because browsers silently scale.


The old single-file 06 Composites App/work-orders.html stays as an offline backup and archive viewer. It opens any exported JSON with no server at all. Don't delete it.
07 CFD PDF Viewer/ compares Fluent CFD reports without opening two PDFs side by side and hunting for the same plot in each. Load two or more reports: Pages scrolls them together, Panels pulls one named plot out of every report cropped identically, Overlay blends or per-pixel-diffs two, Summary tables the solver settings with changes highlighted, and Search covers everything open. Desktop (Electron) and web are the same code; its README.md has a two-command way to try it on the sample reports.

Pain Points and CS Standards (01, 02) are where the app's rules come from: 10 root-caused SN5 problems, each mapped to a numbered standard that fixes it. CS-INDEX is the lookup and python3 tools/check_traceability.py audits the mapping. Every quantitative claim cites a datasheet in 03 Datasheets/ or a recorded team measurement, and every standard ships "Draft, pending Lead signature" until someone signs the approval table.
Datasheets and Printables (04, 05) are reference material: manufacturer TDS/SDS PDFs chosen from actual purchase history, and shop-floor sheets meant to be printed.
Design System (06) is the app's visual language pulled out into something reusable: tokens, component styles, and a living style guide in light and dark. The app remains the source of truth; tools/test_designsystem.mjs keeps the two from drifting apart.
Open items (need a human): move the feb-composites Firebase project to a team Google account (or add the next lead as an owner) so it survives handoff; confirm the ShopSabre's exact model against CS-005 §5; field-verify the CS-011 storage map at RFS; sign the approval tables. HANDOFF.md carries the full list.
Maintenance: the standards are edited as Google Docs (02 CS Standards/GOOGLE-DOCS.md has the folder and per-doc links); edits sync back into 02 CS Standards/src/ with a revision bump, then rebuild with tools/.venv/bin/python tools/build_docx.py --all, then python3 tools/gen_docs_manifest.py and python3 tools/check_traceability.py. Figures are edited as SVG in src/figures/ and re-rendered with node tools/render_figures.mjs before the rebuild. Regenerate retro work orders only if the source data was wrong.
The full inventory, what each suite covers, which need Playwright or the Firebase emulator, and the hard-won lessons behind the browser tests all live in tools/README.md. The short version:
node tools/test_app.mjs # app logic, no browser, run this first
node tools/test_designsystem.mjs # CSS drift between app and 06, ~1s
node tools/test_appui.mjs # every tab, four widths, two themes, measured
node tools/test_detailui.mjs # the same with records open and fields full
plus suites for the mold slicer and packer, the drawings, printing on phones, labels and QR codes, scanning, the public scan page, the sanitizer, safe-area insets, the website, and the three Firebase rules files. Before shipping anything visual, run the matching browser suite and look at the screenshots it can write with --shots; the tests measure, but only eyes catch "unreadable". After a UI change, node tools/make_mockups.mjs regenerates the annotated screenshots in this and the other READMEs.
One quirk worth knowing: the git root is this folder rather than 06 Composites App/, because the scripts in tools/ resolve their paths relative to here. firebase deploy still has to run from inside 06 Composites App/.
Two Claude Code mods live in .claude/skills/ and load in every Claude session opened on this repo. They turn rules from CLAUDE.md into checks instead of things a session has to remember.
feb-guard refuses, before it runs: a bare firebase deploy; --only with anything but hosting (Simon typing "allow rules deploy" or "allow functions deploy" lifts that for one turn); a hosting deploy while 06 Composites App/ is dirty or has unpushed commits; gh release create/upload/edit/delete and addin-v* tags, since tools/release.mjs is the only release path; sed -i or an Edit/Write that changes APP_VERSION, ADDIN_VERSION or the add-in manifest version; a force-push to main; and an SSH remote.
feb-deploy-verify runs after a hosting deploy that reports success. It fetches core.js and the last commit's changed text files off feb-composites.web.app, compares them byte for byte with the commit, and tells the session plainly when they differ. Deploys made inside release.mjs are not seen by either mod; the script verifies those itself.
Their tests run with claude plugin test .claude/skills/feb-guard (and the same for feb-deploy-verify). The claude on PATH has to be a build that knows the plugin test command.
hooks/register.ts 63 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { filesToCheck, isHostingDeploy, summarize, versionIn } from './verify'
4import type { FileCheck } from './verify'
5
6const SCOPE = '/composites_programs/'
7const HOST = 'https://feb-composites.web.app'
8const PUBLIC = '06 Composites App/app/'
9
10// "Deploy complete" from the CLI is not the check (SN6 Resources/CLAUDE.md).
11// After a hosting deploy succeeds, fetch what changed off the live host and
12// compare it byte for byte with the commit.
13export const register: Register = on => {
14 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
15 const ran = await next(e)
16 if (ran.deny !== undefined || ran.isError || !isHostingDeploy(e.command)) return ran
17 if (!/Deploy complete/i.test(ran.text ?? '')) return ran
18
19 const cwd = await $.session.cwd()
20 if (!cwd.includes(SCOPE)) return ran
21
22 let note: string
23 try {
24 note = await verify($, cwd)
25 } catch (err) {
26 note = `feb-deploy-verify could not run: ${String(err)}. Check the live host by hand.`
27 }
28 return { ...ran, context: [...(ran.context ?? []), note] }
29 })
30}
31
32async function verify($: EngineInterface, cwd: string): Promise<string> {
33 const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd })
34 if (top.exitCode !== 0) return 'feb-deploy-verify: not in a git repo, skipped.'
35 const root = top.stdout.trim()
36
37 const head = (await $.process.run(['git', 'rev-parse', '--short', 'HEAD'], { cwd: root })).stdout.trim()
38 const diff = await $.process.run(['git', 'diff', '--name-only', 'HEAD~1', 'HEAD', '--', PUBLIC], { cwd: root })
39 const paths = filesToCheck(diff.exitCode === 0 ? diff.stdout : '', PUBLIC)
40
41 const check = async (): Promise<FileCheck[]> => {
42 const stamp = await $.clock.now()
43 return Promise.all(paths.map(async rel => {
44 const local = await $.fs.read(`${root}/${PUBLIC}${rel}`)
45 const res = await $.http.fetch(`${HOST}/${rel}?verify=${stamp}`)
46 return { rel, ok: res.ok, status: res.status, matches: res.ok && res.text === local, local, live: res.text }
47 }))
48 }
49
50 // The CDN can lag a few seconds behind the CLI; one retry covers it.
51 let results = await check()
52 if (results.some(r => !r.matches)) {
53 await $.clock.sleep(5000)
54 results = await check()
55 }
56
57 const core = results.find(r => r.rel === 'core.js')
58 const versions = core ? { local: versionIn(core.local), live: versionIn(core.live) } : undefined
59 const { ok, text } = summarize(results, head, versions)
60 $.ui.toast(ok ? `Deploy verified: ${results.length} file(s) live at ${head}` : 'Deploy NOT verified: live host differs')
61 return text
62}
63hooks/verify.ts 42 lines1// Pure helpers, no `$`, so the tests can drive them directly.
2
3export type FileCheck = { rel: string; ok: boolean; status: number; matches: boolean; local: string; live: string }
4
5const TEXT = /\.(js|mjs|html|css|json)$/
6const MAX = 8
7
8export function isHostingDeploy(command: string): boolean {
9 return /\bfirebase\s+deploy\b[^\n;&|]*--only[= ]\s*["']?[^\s"']*\bhosting\b/.test(command)
10}
11
12// The files the last commit changed under the hosting root, as paths relative
13// to it, plus core.js always (it carries APP_VERSION). Text only, capped.
14export function filesToCheck(diffNames: string, publicDir: string): string[] {
15 const changed = diffNames.split('\n').map(s => s.trim())
16 .filter(s => s.startsWith(publicDir))
17 .map(s => s.slice(publicDir.length))
18 .filter(s => TEXT.test(s) && !s.split('/').some(p => p.startsWith('.')))
19 return ['core.js', ...changed.filter(s => s !== 'core.js')].slice(0, MAX)
20}
21
22export function versionIn(text: string): string | undefined {
23 return text.match(/\bAPP_VERSION\s*=\s*["']([^"']+)["']/)?.[1]
24}
25
26export function summarize(
27 results: FileCheck[],
28 head: string,
29 versions?: { local?: string; live?: string },
30): { ok: boolean; text: string } {
31 const bad = results.filter(r => !r.matches)
32 const v = versions ? ` APP_VERSION local ${versions.local ?? '?'}, live ${versions.live ?? '?'}.` : ''
33 if (!bad.length) {
34 return { ok: true, text: `feb-deploy-verify: the live host matches commit ${head} for all ${results.length} checked file(s) (${results.map(r => r.rel).join(', ')}).${v}` }
35 }
36 const lines = bad.map(r => `- ${r.rel}: ${r.ok ? 'live content differs from the commit' : `HTTP ${r.status}`}`)
37 return {
38 ok: false,
39 text: `feb-deploy-verify: the live host does NOT match commit ${head}.${v}\n${lines.join('\n')}\nDo not report this deploy as done. Check whether the deploy ran from the right tree, then redeploy.`,
40 }
41}
42