From d8f0055f988a8f807d5ed66d76841e6352029d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jim=C3=A9nez?= Date: Fri, 30 Oct 2026 23:20:44 -0700 Subject: [PATCH 1/4] feat(floe): show Floe's own Verified values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Floe now has an OpenSSF entry: Silver, and Baseline Level 3. Its page said those checks were still to come, so it now draws the same cells as Thaw's, with its SLSA level, Scorecard and test coverage, each linking to Floe's own record. Signed-off-by: René Jiménez --- README.md | 2 +- components/assurance.tsx | 47 ++++++++++++++++++++++++++------------- components/floe-parts.tsx | 44 ++++++++---------------------------- e2e/smoke.spec.ts | 14 ++++++++++++ lib/floe-community.json | 40 ++++++++++++++++++--------------- lib/project.json | 17 +++++++++++--- lib/shared.ts | 1 - lib/verified.test.mjs | 11 +++++++++ lib/verified.ts | 28 ++++++++++++++++------- scripts/sync/config.mjs | 7 +++--- 10 files changed, 126 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index dcb223e..643b413 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ build or blanks a number. | File | What | From | | ------------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------- | | `lib/community.json` | Stars, downloads, contributors, translators, the Verified values | GitHub, Homebrew, Discord, OpenSSF, SonarQube Cloud, `CREDITS.md` | -| `lib/floe-community.json` | The same for Floe, for `/floe/community` | The same sources, for `thaw-app/Floe` | +| `lib/floe-community.json` | The same for Floe, for `/floe` and `/floe/community` | The same sources, for `thaw-app/Floe` | | `lib/community.json` (`world`) | Stars, issues and pull requests by country | Every stargazer's and author's GitHub profile location | | `lib/roadmap-issues.json` | Whether each issue the roadmap links is open | GitHub | | `lib/roadmap-requests.json` | The open feature requests, most thumbs-up first | GitHub | diff --git a/components/assurance.tsx b/components/assurance.tsx index 448d9b7..bb0b8d3 100644 --- a/components/assurance.tsx +++ b/components/assurance.tsx @@ -3,28 +3,37 @@ import Link from 'next/link'; import { ResolvingText } from '@/components/resolving-text'; import community from '@/lib/community.json'; import { compact } from '@/lib/shared'; -import { verifiedFrom } from '@/lib/verified'; - -const { assurance } = community; +import { type Verified, verifiedFrom } from '@/lib/verified'; // What outside bodies have verified about how Thaw is built and released. -export const verified = verifiedFrom(assurance); - -const stepped = verified.length === 5; +export const verified = verifiedFrom(community.assurance); -const numbers = [ - { value: community.stars, label: 'GitHub stars' }, - { value: community.downloads, label: 'downloads' }, - { value: community.contributors, label: 'contributors' }, - { value: community.releases, label: 'releases' }, -].filter((entry) => typeof entry.value === 'number'); +/** The counts from a product's community file that sit under its values. */ +export const countsFrom = ( + counts: Record<'stars' | 'downloads' | 'contributors' | 'releases', number>, +) => + [ + { value: counts.stars, label: 'GitHub stars' }, + { value: counts.downloads, label: 'downloads' }, + { value: counts.contributors, label: 'contributors' }, + { value: counts.releases, label: 'releases' }, + ].filter((entry) => typeof entry.value === 'number'); /** - * How the project is run, small enough to read in passing: what has been independently + * How a project is run, small enough to read in passing: what has been independently * verified about its releases, and how many people use and build it. /verified explains * each one. */ -export function Assurance() { +export function VerifiedBlock({ + values, + numbers, + communityHref, +}: { + values: Verified[]; + numbers: ReturnType; + communityHref: string; +}) { + const stepped = values.length === 5; return ( <> @@ -36,7 +45,7 @@ export function Assurance() {
    - {verified.map((entry, index) => ( + {values.map((entry, index) => (
  • What does this mean? - + Community

    @@ -94,3 +103,9 @@ export function Assurance() { ); } + +export function Assurance() { + return ( + + ); +} diff --git a/components/floe-parts.tsx b/components/floe-parts.tsx index b8e73bb..1120bc9 100644 --- a/components/floe-parts.tsx +++ b/components/floe-parts.tsx @@ -1,9 +1,10 @@ -import { Heading } from 'fumadocs-ui/components/heading'; import { Download } from 'lucide-react'; import Link from 'next/link'; +import { countsFrom, VerifiedBlock } from '@/components/assurance'; import { FeatureCells, MarkRow, NameGroups, WorksWith } from '@/components/feature-list'; import { InstallFacts } from '@/components/install-facts'; import { SectionLabel } from '@/components/section-label'; +import community from '@/lib/floe-community.json'; import { floeAlso, floeAskAi, @@ -14,6 +15,7 @@ import { } from '@/lib/floe-features'; import { latestStable } from '@/lib/releases'; import { docsRoute } from '@/lib/shared'; +import { verifiedFrom } from '@/lib/verified'; /** How to get Floe: one download, set out as Thaw's install box is (install-tabs.tsx). */ export function FloeInstall() { @@ -71,41 +73,13 @@ export function FloeFeatures() { ); } -/** - * Where Thaw's page has its Verified values, Floe's says they are on the way: Floe is early - * and has none of its own to show yet. - */ +/** Floe's own Verified values, read by the same sync as Thaw's. */ export function FloeAssurance() { return ( - <> - - Verified - -
    -

    - Part of the way. -

    -

    - Floe is signed and notarised, each release comes with a record of how it was built, and - the project has an{' '} - - OpenSSF Scorecard - - . Thaw’s other checks, such as its OpenSSF Best Practices badge, are still to come. -

    - - See where Thaw stands - - SLSA, OpenSSF and test coverage - - -
    - + ); } diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 4a02496..de5c44e 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -66,6 +66,20 @@ test('every Verified value sits under its own name', async ({ page }) => { if ((await coverage.locator('span').count()) > 1) await expect(coverage).toContainText('%'); }); +test('Floe’s page shows its own Verified values', async ({ page }) => { + await page.goto('/floe'); + const section = page.locator('h2#verified + div'); + await expect( + section.locator('a[href*="bestpractices.dev/projects/15396"]').first(), + ).toBeVisible(); + await expect(section.locator('a[href*="github.com/thaw-app/Floe"]').first()).toBeVisible(); + await expect(section.locator('a[href*="projects/13303"]')).toHaveCount(0); + await expect(section.getByRole('link', { name: 'Community' })).toHaveAttribute( + 'href', + '/floe/community', + ); +}); + test('the pages a visitor moves between all answer', async ({ page }) => { for (const path of [ '/', diff --git a/lib/floe-community.json b/lib/floe-community.json index f7462d8..6703147 100644 --- a/lib/floe-community.json +++ b/lib/floe-community.json @@ -1,26 +1,28 @@ { "stars": 167, - "downloads": 297, - "discord": 384, + "downloads": 310, + "discord": 385, "contributors": 4, "releases": 4, "issuesFixed": 6, "readOn": { - "stars": "2026-10-30", - "downloads": "2026-10-30", - "discord": "2026-10-30", - "contributors": "2026-10-30", - "releases": "2026-10-30", - "issuesFixed": "2026-10-30", - "scorecard": "2026-10-30", - "coverage": "2026-10-30", - "field": "2026-10-30" + "stars": "2026-10-31", + "downloads": "2026-10-31", + "discord": "2026-10-31", + "contributors": "2026-10-31", + "releases": "2026-10-31", + "issuesFixed": "2026-10-31", + "bestPractices": "2026-10-31", + "baseline": "2026-10-31", + "scorecard": "2026-10-31", + "coverage": "2026-10-31", + "field": "2026-10-31" }, "people": [ { "login": "diazdesandi", "avatar": "https://avatars.githubusercontent.com/u/17421494?v=4", - "commits": 196 + "commits": 198 }, { "login": "lylythechosenone", @@ -46,17 +48,19 @@ } ], "assurance": { - "scorecard": 7.2, + "bestPractices": "silver", + "baseline": 3, + "scorecard": 8.1, "coverage": 86, "field": { - "projects": 13467, + "projects": 13507, "gold": 112, "baseline3": 100 } }, "translators": [], "world": { - "checked": "2026-10-30", + "checked": "2026-10-31", "stargazers": { "people": 167, "placed": 71, @@ -190,13 +194,13 @@ ] }, "pulls": { - "people": 39, - "placed": 29, + "people": 41, + "placed": 31, "list": [ { "code": "MX", "name": "Mexico", - "people": 26 + "people": 28 }, { "code": "TW", diff --git a/lib/project.json b/lib/project.json index 57a8d07..3cd57f2 100644 --- a/lib/project.json +++ b/lib/project.json @@ -1,11 +1,22 @@ { "org": "thaw-app", "products": { - "thaw": { "name": "Thaw", "repo": "thaw-app/Thaw", "ref": "development", "tone": "#fa9a2a" }, - "floe": { "name": "Floe", "repo": "thaw-app/Floe", "ref": "main", "tone": "#4f93ff" } + "thaw": { + "name": "Thaw", + "repo": "thaw-app/Thaw", + "ref": "development", + "tone": "#fa9a2a", + "bestPractices": 13303 + }, + "floe": { + "name": "Floe", + "repo": "thaw-app/Floe", + "ref": "main", + "tone": "#4f93ff", + "bestPractices": 15396 + } }, "discordInvite": "KDfWjWDnR4", - "bestPractices": 13303, "links": { "crowdin": "https://crowdin.com/project/thaw", "sponsors": "https://github.com/sponsors/stonerl", diff --git a/lib/shared.ts b/lib/shared.ts index dcc0bb9..5042354 100644 --- a/lib/shared.ts +++ b/lib/shared.ts @@ -87,7 +87,6 @@ export const links = { github: `https://github.com/${project.org}`, discord: `https://discord.gg/${project.discordInvite}`, /** The project's entry with OpenSSF, which holds its Best Practices badge and Baseline level. */ - bestPractices: `https://www.bestpractices.dev/projects/${project.bestPractices}`, }; /** A count as people say it: 11.8K, 365.4K. */ diff --git a/lib/verified.test.mjs b/lib/verified.test.mjs index 74236f1..c1222c2 100644 --- a/lib/verified.test.mjs +++ b/lib/verified.test.mjs @@ -24,6 +24,17 @@ test('a value that could not be read does not shift the others under the wrong n }); }); +test('Floe’s values point at Floe, not at Thaw', () => { + const values = verifiedFrom({ ...full, bestPractices: 'silver' }, 'floe'); + const href = (id) => values.find((value) => value.id === id)?.href; + expect(href('slsa')).toBe('https://github.com/thaw-app/Floe/attestations'); + expect(href('bestPractices')).toBe('https://www.bestpractices.dev/projects/15396'); + expect(href('baseline')).toBe('https://www.bestpractices.dev/projects/15396'); + expect(href('scorecard')).toContain('github.com/thaw-app/Floe'); + expect(href('coverage')).toContain('thaw-app_Floe'); + expect(values.find((value) => value.id === 'coverage')?.detail).toContain('Floe’s code'); +}); + test('only the highest badge and the top level are called that', () => { const said = (assurance, id) => verifiedFrom(assurance).find((entry) => entry.id === id)?.detail; expect(said(full, 'bestPractices')).toContain('The highest of three badges'); diff --git a/lib/verified.ts b/lib/verified.ts index a7f56a0..0dfcd1e 100644 --- a/lib/verified.ts +++ b/lib/verified.ts @@ -1,7 +1,18 @@ -import { docsRoute, links, products } from '@/lib/shared'; +import { docsRoute, type ProductSlug, products } from '@/lib/shared'; const repo = products.thaw.repo; +/** A product's entry on OpenSSF's Best Practices list, which holds its badge and Baseline level. */ +const bestPracticesUrl = (slug: ProductSlug) => + `https://www.bestpractices.dev/projects/${products[slug].bestPractices}`; + +// Where someone checks a release's SLSA record. Floe's guide is on its development branch, and +// the docs here follow main, so until that moves its attestations stand in. +const provenance: Record = { + thaw: `${docsRoute}/thaw/verifying-releases`, + floe: `https://github.com/${products.floe.repo}/attestations`, +}; + // What each of those is, for someone who has not met them. `checkedBy` says who vouches // for the value, since a score a machine gives and a checklist a project fills in differ. // `id` is what ties one to its value: a value that could not be read is left out of the @@ -27,7 +38,7 @@ export const meanings: { meaning: 'A checklist from the Open Source Security Foundation for how a project is run, such as how changes get reviewed and how security reports are handled. It has three badges, Passing, Silver and Gold.', checkedBy: 'The project answers each item and publishes its evidence.', - href: links.bestPractices, + href: bestPracticesUrl('thaw'), }, { id: 'baseline', @@ -98,12 +109,13 @@ const badges = ['passing', 'silver', 'gold']; const places = ['lowest', 'second', 'highest']; /** - * The Verified values as the pages show them. Each is read from its source when the site + * A product's Verified values as the pages show them. Each is read from its source when the site * is built, apart from the SLSA level, which the release pipeline documents; one that * could not be read is left out. What is said beside a value follows from the value, so a * badge that is not the highest is never called it. */ -export function verifiedFrom(assurance: Assurance): Verified[] { +export function verifiedFrom(assurance: Assurance, slug: ProductSlug = 'thaw'): Verified[] { + const { name, repo } = products[slug]; const { bestPractices, baseline, scorecard, coverage } = assurance; const badge = bestPractices ? badges.indexOf(bestPractices) : -1; const candidates: (Verified | false)[] = [ @@ -115,7 +127,7 @@ export function verifiedFrom(assurance: Assurance): Verified[] { steps: 3, reached: 3, tone: green, - href: `${docsRoute}/thaw/verifying-releases`, + href: provenance[slug], }, badge >= 0 && typeof bestPractices === 'string' && { @@ -126,7 +138,7 @@ export function verifiedFrom(assurance: Assurance): Verified[] { steps: 3, reached: badge + 1, tone: bestPractices === 'gold' ? gold : green, - href: links.bestPractices, + href: bestPracticesUrl(slug), }, typeof baseline === 'number' && baseline >= 1 && { @@ -139,7 +151,7 @@ export function verifiedFrom(assurance: Assurance): Verified[] { steps: 3, reached: baseline, tone: green, - href: links.bestPractices, + href: bestPracticesUrl(slug), }, typeof scorecard === 'number' && { id: 'scorecard', @@ -154,7 +166,7 @@ export function verifiedFrom(assurance: Assurance): Verified[] { id: 'coverage', value: `${coverage}%`, label: 'Test coverage', - detail: 'The share of Thaw’s code its tests run.', + detail: `The share of ${name}’s code its tests run.`, share: coverage / 100, tone: green, href: `https://sonarcloud.io/component_measures?id=${repo.replace('/', '_')}&metric=coverage`, diff --git a/scripts/sync/config.mjs b/scripts/sync/config.mjs index 1060071..186b4dc 100644 --- a/scripts/sync/config.mjs +++ b/scripts/sync/config.mjs @@ -49,7 +49,7 @@ const products = [ updatesRepo: 'thaw-app/updates', team: ['stonerl', 'diazdesandi', 'nightah', 'alvst', 'camguillory'], // The project's entry on bestpractices.dev, which holds its OpenSSF badge and Baseline level. - bestPractices: project.bestPractices, + bestPractices: project.products.thaw.bestPractices, }, // Pages this site writes itself, so the repo's file of the same name is not synced in. // The roadmap is content/site/roadmap.mdx, drawn at /roadmap. @@ -160,12 +160,13 @@ const products = [ // Floe's repo began on this day, so every commit in it counts. contributorsSince: '2026-10-03', notPeople: ['claude', 'cursoragent', 'lathe-agent-oa'], - // The numbers on Floe's community page (/floe/community). It has no Homebrew cask and no - // OpenSSF entry yet, so those are not asked for. It shares Thaw's Discord. + // The numbers on Floe's community page (/floe/community). It has no Homebrew cask, so + // that is not asked for. It shares Thaw's Discord. community: { file: 'floe-community.json', discordInvite: project.discordInvite, team: ['diazdesandi'], + bestPractices: project.products.floe.bestPractices, }, // Floe's README is laid out as Thaw's is, and its page leaves out what the home page // does, for the same reasons. Its roadmap is a page of its own, written in From a660e71e1eb076eab7546f3e18411832a341d4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jim=C3=A9nez?= Date: Fri, 30 Oct 2026 23:41:28 -0700 Subject: [PATCH 2/4] docs(floe): follow the development branch, and list Floe's new docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Floe's main is behind development, its default branch, so the site was missing its new docs. Following development adds five pages, lets the SLSA cell link to Floe's own verifying guide, and reads the Rust crates from where they now live. Signed-off-by: René Jiménez --- README.md | 2 +- content/docs/floe/meta.json | 5 +++++ e2e/smoke.spec.ts | 4 +++- lib/floe-community.json | 9 +++++++-- lib/project.json | 2 +- lib/verified.test.mjs | 2 +- lib/verified.ts | 9 +-------- scripts/built-with.mjs | 2 +- scripts/sync/config.mjs | 21 +++++++++++++++++++-- 9 files changed, 39 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 643b413..7c2c563 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ On a touch device the home page's cube redraws twelve times a second. bun run dev # syncs the docs, then serves http://localhost:3000 By default the sync fetches each repo's default branch from GitHub -(`development` for Thaw, `main` for Floe). To +(`development` for both). To preview docs you have not pushed, point it at a local checkout in `.env.local`: THAW_DOCS_DIR=/path/to/Thaw diff --git a/content/docs/floe/meta.json b/content/docs/floe/meta.json index 6e56c6c..04897c6 100644 --- a/content/docs/floe/meta.json +++ b/content/docs/floe/meta.json @@ -10,7 +10,12 @@ "---Developers---", "contribute", "development", + "coding-standards", + "architecture", + "interfaces", + "verifying-releases", "security", + "assurance-case", "!readme", "..." ] diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index de5c44e..99f0de1 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -72,7 +72,9 @@ test('Floe’s page shows its own Verified values', async ({ page }) => { await expect( section.locator('a[href*="bestpractices.dev/projects/15396"]').first(), ).toBeVisible(); - await expect(section.locator('a[href*="github.com/thaw-app/Floe"]').first()).toBeVisible(); + await section.locator('a[href="/docs/floe/verifying-releases"]').click(); + await expect(page.getByRole('heading', { level: 1 })).toContainText('Verifying'); + await page.goBack(); await expect(section.locator('a[href*="projects/13303"]')).toHaveCount(0); await expect(section.getByRole('link', { name: 'Community' })).toHaveAttribute( 'href', diff --git a/lib/floe-community.json b/lib/floe-community.json index 6703147..9289979 100644 --- a/lib/floe-community.json +++ b/lib/floe-community.json @@ -1,8 +1,8 @@ { "stars": 167, - "downloads": 310, + "downloads": 311, "discord": 385, - "contributors": 4, + "contributors": 5, "releases": 4, "issuesFixed": 6, "readOn": { @@ -34,6 +34,11 @@ "avatar": "https://avatars.githubusercontent.com/u/132474703?v=4", "commits": 2 }, + { + "login": "MintCollector", + "avatar": "https://avatars.githubusercontent.com/u/158844909?v=4", + "commits": 1 + }, { "login": "OwenCope", "avatar": "https://avatars.githubusercontent.com/u/135015709?v=4", diff --git a/lib/project.json b/lib/project.json index 3cd57f2..0c9cdb5 100644 --- a/lib/project.json +++ b/lib/project.json @@ -11,7 +11,7 @@ "floe": { "name": "Floe", "repo": "thaw-app/Floe", - "ref": "main", + "ref": "development", "tone": "#4f93ff", "bestPractices": 15396 } diff --git a/lib/verified.test.mjs b/lib/verified.test.mjs index c1222c2..090375d 100644 --- a/lib/verified.test.mjs +++ b/lib/verified.test.mjs @@ -27,7 +27,7 @@ test('a value that could not be read does not shift the others under the wrong n test('Floe’s values point at Floe, not at Thaw', () => { const values = verifiedFrom({ ...full, bestPractices: 'silver' }, 'floe'); const href = (id) => values.find((value) => value.id === id)?.href; - expect(href('slsa')).toBe('https://github.com/thaw-app/Floe/attestations'); + expect(href('slsa')).toBe('/docs/floe/verifying-releases'); expect(href('bestPractices')).toBe('https://www.bestpractices.dev/projects/15396'); expect(href('baseline')).toBe('https://www.bestpractices.dev/projects/15396'); expect(href('scorecard')).toContain('github.com/thaw-app/Floe'); diff --git a/lib/verified.ts b/lib/verified.ts index 0dfcd1e..06b5bc8 100644 --- a/lib/verified.ts +++ b/lib/verified.ts @@ -6,13 +6,6 @@ const repo = products.thaw.repo; const bestPracticesUrl = (slug: ProductSlug) => `https://www.bestpractices.dev/projects/${products[slug].bestPractices}`; -// Where someone checks a release's SLSA record. Floe's guide is on its development branch, and -// the docs here follow main, so until that moves its attestations stand in. -const provenance: Record = { - thaw: `${docsRoute}/thaw/verifying-releases`, - floe: `https://github.com/${products.floe.repo}/attestations`, -}; - // What each of those is, for someone who has not met them. `checkedBy` says who vouches // for the value, since a score a machine gives and a checklist a project fills in differ. // `id` is what ties one to its value: a value that could not be read is left out of the @@ -127,7 +120,7 @@ export function verifiedFrom(assurance: Assurance, slug: ProductSlug = 'thaw'): steps: 3, reached: 3, tone: green, - href: provenance[slug], + href: `${docsRoute}/${slug}/verifying-releases`, }, badge >= 0 && typeof bestPractices === 'string' && { diff --git a/scripts/built-with.mjs b/scripts/built-with.mjs index 46571e8..1226619 100644 --- a/scripts/built-with.mjs +++ b/scripts/built-with.mjs @@ -24,7 +24,7 @@ const apps = { ref: process.env.FLOE_DOCS_REF || project.products.floe.ref, swift: 'Package.resolved', javascript: 'runtime/package.json', - rust: 'Vendor/FendCore/rust/Cargo.toml', + rust: 'Vendor/FloeRust/rust/Cargo.toml', }, }; diff --git a/scripts/sync/config.mjs b/scripts/sync/config.mjs index 186b4dc..15f52c5 100644 --- a/scripts/sync/config.mjs +++ b/scripts/sync/config.mjs @@ -186,12 +186,29 @@ const products = [ features: '', last: '', }, - titles: {}, + titles: { + architecture: 'Architecture', + 'verifying-releases': 'Verifying releases', + 'assurance-case': 'Security assurance case', + }, descriptions: { development: 'How to build and run Floe from source, how it runs Raycast extensions, and how to add one.', + architecture: 'A map of Floe’s processes and code, and where each part lives.', + 'coding-standards': 'How code in Floe is written.', + interfaces: 'What a released Floe accepts from other software, and what it reaches out to.', + 'verifying-releases': + 'What each Floe release is signed with, and how to check a download yourself.', + 'assurance-case': + 'The case that Floe meets its security requirements, and the risks that remain.', + }, + icons: { + changelog: 'ScrollText', + development: 'Hammer', + architecture: 'Layers', + 'verifying-releases': 'ShieldCheck', + 'assurance-case': 'FileCheck', }, - icons: { changelog: 'ScrollText', development: 'Hammer' }, extraPages: { 'CHANGELOG.md': 'changelog', // Shown at /floe, not in the docs sidebar. From e846cd4a0a8adb31a45af744c351c9f9f8b6405f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jim=C3=A9nez?= Date: Fri, 30 Oct 2026 23:44:08 -0700 Subject: [PATCH 3/4] feat(floe): give Floe's Markdown page its Verified values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /floe.md left out what /floe now shows, and nowhere else in Markdown had Floe's values. The smoke test now checks the values on both, not only the links. Signed-off-by: René Jiménez --- e2e/smoke.spec.ts | 21 ++++++++++++++++++++- lib/site-markdown.ts | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 99f0de1..9c669b7 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,5 +1,6 @@ import { createHash } from 'node:crypto'; import { expect, type Page, test } from '@playwright/test'; +import floeCommunity from '../lib/floe-community.json'; test('the install tabs work from the keyboard and give the right command', async ({ page }) => { await page.goto('/'); @@ -66,9 +67,27 @@ test('every Verified value sits under its own name', async ({ page }) => { if ((await coverage.locator('span').count()) > 1) await expect(coverage).toContainText('%'); }); -test('Floe’s page shows its own Verified values', async ({ page }) => { +test('Floe’s page shows its own Verified values', async ({ page, request }) => { + const { bestPractices, baseline, scorecard, coverage } = floeCommunity.assurance; + const values = { + 'SLSA build': 'Level 3', + 'OpenSSF Best Practices': bestPractices.charAt(0).toUpperCase() + bestPractices.slice(1), + 'OpenSSF Baseline': `Level ${baseline}`, + 'OpenSSF Scorecard': `${scorecard} / 10`, + 'Test coverage': `${coverage}%`, + }; await page.goto('/floe'); const section = page.locator('h2#verified + div'); + for (const [label, value] of Object.entries(values)) { + await expect(section.locator('li').filter({ hasText: label }), label).toContainText(value); + } + // Its Markdown says the same, with the same links. + const markdown = await (await request.get('/floe.md')).text(); + for (const [label, value] of Object.entries(values)) { + expect(markdown, label).toContain(`${label}]`); + expect(markdown, label).toContain(`: ${value}.`); + } + expect(markdown).toContain('/docs/floe/verifying-releases'); await expect( section.locator('a[href*="bestpractices.dev/projects/15396"]').first(), ).toBeVisible(); diff --git a/lib/site-markdown.ts b/lib/site-markdown.ts index 6c5b2e6..7f3160a 100644 --- a/lib/site-markdown.ts +++ b/lib/site-markdown.ts @@ -120,6 +120,11 @@ function floe() { '## Ask AI with', floeAskAi.map((item) => `- ${link(item.name, item.href)}`), `Raycast is a trademark of Raycast Technologies Inc. Floe is not affiliated with Raycast. More in the ${link('Floe documentation', `${docsRoute}/floe`)} and on the ${link('Floe roadmap', '/floe/roadmap')}.`, + '## Verified', + verifiedFrom(floeCommunity.assurance, 'floe').map( + (entry) => `- ${link(entry.label, entry.href)}: ${entry.value}. ${entry.detail}`, + ), + `What each of these means: ${link('What Verified means', '/verified')}.`, ); } From 106b8ef3a51a17d6d805f3af31aad27a9d25f74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jim=C3=A9nez?= Date: Fri, 30 Oct 2026 23:49:26 -0700 Subject: [PATCH 4/4] feat(verified): give Floe its own Verified page, with a Thaw and Floe switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "What does this mean?" on Floe's page led to Thaw's values. /floe/verified is the same page with Floe's values and links, and the switch the roadmaps and changelogs have leads between the two. It is in the footer, the sitemap, llms.txt and Markdown. Signed-off-by: René Jiménez --- app/(floe)/floe/verified/page.tsx | 16 +++++ app/(home)/verified/page.tsx | 78 +--------------------- app/sitemap.ts | 1 + components/assurance.tsx | 15 +++-- components/floe-parts.tsx | 1 + components/site-footer.tsx | 2 + components/verified-page.tsx | 103 ++++++++++++++++++++++++++++++ e2e/smoke.spec.ts | 28 ++++++++ lib/llms.ts | 5 ++ lib/site-markdown.ts | 21 +++--- lib/site-pages.ts | 6 ++ lib/verified.test.mjs | 23 ++++++- lib/verified.ts | 33 ++++++---- 13 files changed, 228 insertions(+), 104 deletions(-) create mode 100644 app/(floe)/floe/verified/page.tsx create mode 100644 components/verified-page.tsx diff --git a/app/(floe)/floe/verified/page.tsx b/app/(floe)/floe/verified/page.tsx new file mode 100644 index 0000000..a84ff8e --- /dev/null +++ b/app/(floe)/floe/verified/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next'; +import { VerifiedView } from '@/components/verified-page'; +import { pageAlternates } from '@/lib/releases'; + +export const metadata: Metadata = { + // One address for the page, whichever domain it was reached on. + alternates: pageAlternates('/floe/verified', '/floe/verified.md'), + openGraph: { images: '/og/site/floeVerified/image.png' }, + title: 'What Verified means for Floe', + description: + 'What SLSA, the OpenSSF badges, the Scorecard and test coverage are, who vouches for each, and where Floe stands.', +}; + +export default function FloeVerifiedPage() { + return ; +} diff --git a/app/(home)/verified/page.tsx b/app/(home)/verified/page.tsx index a2f0616..de9a1f1 100644 --- a/app/(home)/verified/page.tsx +++ b/app/(home)/verified/page.tsx @@ -1,14 +1,6 @@ import type { Metadata } from 'next'; -import Link from 'next/link'; -import { verified } from '@/components/assurance'; -import { Lead, PageHeader } from '@/components/page-header'; -import { PageShell } from '@/components/page-shell'; -import { ResolvingText } from '@/components/resolving-text'; -import community from '@/lib/community.json'; +import { VerifiedView } from '@/components/verified-page'; import { pageAlternates } from '@/lib/releases'; -import { longDate } from '@/lib/shared'; -import { sitePages } from '@/lib/site-pages'; -import { meanings, readDays, standing } from '@/lib/verified'; export const metadata: Metadata = { // One address for the page, whichever domain it was reached on. @@ -19,72 +11,6 @@ export const metadata: Metadata = { 'What SLSA, the OpenSSF badges, the Scorecard and test coverage are, who vouches for each, and where Thaw stands.', }; -const read = readDays(community.readOn, { - bestPractices: 'The Best Practices badge', - baseline: 'The Baseline level', - scorecard: 'The Scorecard', - coverage: 'Test coverage', - field: 'The count of projects on OpenSSF’s list', -}); - -/** - * The page behind "What does this mean?" on the home page: each thing in the - * Verified row, what it is, where Thaw stands on it today, and who says so. - */ export default function VerifiedPage() { - return ( - - - - Thaw asks for permissions that reach across your Mac. These are the outside checks on how - it is built and released: what each one is, and who vouches for it. A score a machine - gives is not the same as a checklist a project fills in, so each says which it is.{' '} - {standing(community.assurance)} - - - -
    - {meanings.map((entry) => { - // Matched by what it is: a value that could not be read is missing from the list. - const now = verified.find((value) => value.id === entry.id); - return ( -
    -
    - {entry.term} - {now && ( - - {now.value} - - )} -
    -
    -

    {entry.meaning}

    -

    - Who says so. {entry.checkedBy} -

    -

    - - {new URL(entry.href).hostname.replace(/^www\./, '')} - -

    -
    -
    - ); - })} -
    - -

    - {read.day && `Values read on ${longDate(read.day)}. `} - {read.older.map((entry) => `${entry.name} is from ${longDate(entry.day)}. `)} - The SLSA level is the one Thaw’s release pipeline is built to.{' '} - - How to verify a release yourself - - . -

    -
    - ); + return ; } diff --git a/app/sitemap.ts b/app/sitemap.ts index f6c82b8..99adf33 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -21,6 +21,7 @@ export default function sitemap(): MetadataRoute.Sitemap { ['/floe', newest.floe], ['/floe/roadmap', newest.floe], ['/floe/community', Object.values(floeCommunity.readOn).sort().at(-1)], + ['/floe/verified', Object.values(floeCommunity.readOn).sort().at(-1)], ['/community', read], ['/verified', read], ['/faq', undefined], diff --git a/components/assurance.tsx b/components/assurance.tsx index bb0b8d3..205abc9 100644 --- a/components/assurance.tsx +++ b/components/assurance.tsx @@ -21,17 +21,19 @@ export const countsFrom = ( /** * How a project is run, small enough to read in passing: what has been independently - * verified about its releases, and how many people use and build it. /verified explains - * each one. + * verified about its releases, and how many people use and build it. Its Verified page + * explains each one. */ export function VerifiedBlock({ values, numbers, communityHref, + meaningsHref, }: { values: Verified[]; numbers: ReturnType; communityHref: string; + meaningsHref: string; }) { const stepped = values.length === 5; return ( @@ -92,7 +94,7 @@ export function VerifiedBlock({ ))}

    - + What does this mean? @@ -106,6 +108,11 @@ export function VerifiedBlock({ export function Assurance() { return ( - + ); } diff --git a/components/floe-parts.tsx b/components/floe-parts.tsx index 1120bc9..76443ce 100644 --- a/components/floe-parts.tsx +++ b/components/floe-parts.tsx @@ -80,6 +80,7 @@ export function FloeAssurance() { values={verifiedFrom(community.assurance, 'floe')} numbers={countsFrom(community)} communityHref="/floe/community" + meaningsHref="/floe/verified" /> ); } diff --git a/components/site-footer.tsx b/components/site-footer.tsx index 20be91b..e58f2eb 100644 --- a/components/site-footer.tsx +++ b/components/site-footer.tsx @@ -105,6 +105,8 @@ const feet: Record = { { text: 'What Floe does not do yet', href: `${floeDocs}/limitations` }, { text: 'Developing Floe', href: `${floeDocs}/development` }, { text: 'Security policy', href: `${floeDocs}/security` }, + { text: 'Verifying releases', href: `${floeDocs}/verifying-releases` }, + { text: 'What Verified means', href: '/floe/verified' }, ], }, { diff --git a/components/verified-page.tsx b/components/verified-page.tsx new file mode 100644 index 0000000..b2cecef --- /dev/null +++ b/components/verified-page.tsx @@ -0,0 +1,103 @@ +import Link from 'next/link'; +import { Lead, PageHeader } from '@/components/page-header'; +import { PageShell } from '@/components/page-shell'; +import { ProductChoice } from '@/components/product-choice'; +import { ResolvingText } from '@/components/resolving-text'; +import thawCommunity from '@/lib/community.json'; +import floeCommunity from '@/lib/floe-community.json'; +import { docsRoute, longDate, type ProductSlug, products } from '@/lib/shared'; +import { sitePages } from '@/lib/site-pages'; +import { meaningsFor, readDays, standing, verifiedFrom } from '@/lib/verified'; + +const whose = [ + { name: 'Thaw', href: '/verified' }, + { name: 'Floe', href: '/floe/verified' }, +]; + +// What is different between the two: where the numbers come from and why they matter. +const pages = { + thaw: { + community: thawCommunity, + header: sitePages.verified, + opening: 'Thaw asks for permissions that reach across your Mac.', + }, + floe: { + community: floeCommunity, + header: sitePages.floeVerified, + opening: 'Floe opens your apps and runs extensions on your Mac.', + }, +}; + +/** + * The page behind "What does this mean?" on a product's page: each thing in its Verified + * row, what it is, where the product stands on it today, and who says so. + */ +export function VerifiedView({ product }: { product: ProductSlug }) { + const { community, header, opening } = pages[product]; + const { name } = products[product]; + const verified = verifiedFrom(community.assurance, product); + const read = readDays(community.readOn, { + bestPractices: 'The Best Practices badge', + baseline: 'The Baseline level', + scorecard: 'The Scorecard', + coverage: 'Test coverage', + field: 'The count of projects on OpenSSF’s list', + }); + return ( + +

    + + + {opening} These are the outside checks on how it is built and released: what each one + is, and who vouches for it. A score a machine gives is not the same as a checklist a + project fills in, so each says which it is. {standing(community.assurance, product)} + + + +
    + +
    + {meaningsFor(product).map((entry) => { + // Matched by what it is: a value that could not be read is missing from the list. + const now = verified.find((value) => value.id === entry.id); + return ( +
    +
    + {entry.term} + {now && ( + + {now.value} + + )} +
    +
    +

    {entry.meaning}

    +

    + Who says so. {entry.checkedBy} +

    +

    + + {new URL(entry.href).hostname.replace(/^www\./, '')} + +

    +
    +
    + ); + })} +
    + +

    + {read.day && `Values read on ${longDate(read.day)}. `} + {read.older.map((entry) => `${entry.name} is from ${longDate(entry.day)}. `)} + The SLSA level is the one {name}’s release pipeline is built to.{' '} + + How to verify a release yourself + + . +

    + + ); +} diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 9c669b7..2cb9095 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -99,6 +99,32 @@ test('Floe’s page shows its own Verified values', async ({ page, request }) => 'href', '/floe/community', ); + await expect(section.getByRole('link', { name: 'What does this mean?' })).toHaveAttribute( + 'href', + '/floe/verified', + ); +}); + +test('the Verified page has one page for each product, and the way to the other', async ({ + page, +}) => { + await page.goto('/verified'); + const whose = page.getByRole('navigation', { name: 'Whose checks' }); + await expect(whose.getByRole('link', { name: 'Thaw' })).toHaveAttribute('aria-current', 'page'); + await expect(page.locator('dl > div').filter({ hasText: 'Test coverage' })).toContainText( + 'Thaw’s code', + ); + await whose.getByRole('link', { name: 'Floe' }).click(); + await expect(page).toHaveURL(/\/floe\/verified$/); + await expect(whose.getByRole('link', { name: 'Floe' })).toHaveAttribute('aria-current', 'page'); + const rows = page.locator('dl > div'); + await expect(rows.filter({ hasText: 'Test coverage' })).toContainText('Floe’s code'); + await expect(rows.filter({ hasText: 'OpenSSF Best Practices' }).locator('dt')).toContainText( + floeCommunity.assurance.bestPractices.replace(/^./, (first) => first.toUpperCase()), + ); + await expect( + page.locator('header').first().getByRole('link', { name: 'Floe, home' }), + ).toBeVisible(); }); test('the pages a visitor moves between all answer', async ({ page }) => { @@ -107,6 +133,7 @@ test('the pages a visitor moves between all answer', async ({ page }) => { '/floe', '/floe/roadmap', '/floe/community', + '/floe/verified', '/community', '/verified', '/roadmap', @@ -419,6 +446,7 @@ test('AI tools get each page as Markdown, a Markdown not-found, and a skill', as '/privacy', '/community', '/floe/community', + '/floe/verified', '/verified', '/built-with', '/changelog/2.1.0', diff --git a/lib/llms.ts b/lib/llms.ts index 8859a6a..ea27824 100644 --- a/lib/llms.ts +++ b/lib/llms.ts @@ -40,6 +40,11 @@ const sitePages: Record = { '/floe/community', 'Floe in numbers, where to find the people behind it, and who has contributed.', ], + [ + 'What Verified means for Floe', + '/floe/verified', + 'What SLSA, the OpenSSF badges, the Scorecard and test coverage are, who vouches for each, and where Floe stands.', + ], ['Floe changelog', changelogUrl('floe'), 'Every Floe release and what it changed.'], ['Floe release feed', feedUrl('floe'), 'Floe’s releases as an Atom feed.'], ], diff --git a/lib/site-markdown.ts b/lib/site-markdown.ts index 7f3160a..d044f04 100644 --- a/lib/site-markdown.ts +++ b/lib/site-markdown.ts @@ -42,7 +42,7 @@ import { sitePages } from '@/lib/site-pages'; import { docsLlms, source } from '@/lib/source'; import { thawFaq } from '@/lib/thaw-faq'; import { thawAlso, thawFeatures, thawWorksWith } from '@/lib/thaw-features'; -import { meanings, standing, verifiedFrom } from '@/lib/verified'; +import { meaningsFor, standing, verifiedFrom } from '@/lib/verified'; // The site's own pages as Markdown, for AI tools: what each page says, read from the same // data the page is drawn from, so the two cannot come to say different things. A page @@ -124,7 +124,7 @@ function floe() { verifiedFrom(floeCommunity.assurance, 'floe').map( (entry) => `- ${link(entry.label, entry.href)}: ${entry.value}. ${entry.detail}`, ), - `What each of these means: ${link('What Verified means', '/verified')}.`, + `What each of these means: ${link('What Verified means', '/floe/verified')}.`, ); } @@ -265,12 +265,14 @@ function floeCommunityPage() { ); } -function verifiedPage() { - const now = verifiedFrom(community.assurance); +function verifiedPage(slug: ProductSlug) { + const assurance = slug === 'floe' ? floeCommunity.assurance : community.assurance; + const { name } = products[slug]; + const now = verifiedFrom(assurance, slug); return page( - 'What Verified means', - `${sitePages.verified.title} These are the outside checks on how Thaw is built and released: what each one is, where Thaw stands, and who vouches for it. ${standing(community.assurance)}`, - ...meanings.map((entry) => { + `What Verified means for ${name}`, + `${sitePages.verified.title} These are the outside checks on how ${name} is built and released: what each one is, where ${name} stands, and who vouches for it. ${standing(assurance, slug)}`, + ...meaningsFor(slug).map((entry) => { const value = now.find((item) => item.id === entry.id)?.value; return [ `## ${entry.term}${value ? `: ${value}` : ''}`, @@ -280,7 +282,7 @@ function verifiedPage() { `Who says so: ${entry.checkedBy} ${link(new URL(entry.href).hostname.replace(/^www\./, ''), entry.href)}`, ].join('\n'); }), - `How to check a release yourself: ${link('Verifying releases', `${docsRoute}/thaw/verifying-releases`)}.`, + `How to check a release yourself: ${link('Verifying releases', `${docsRoute}/${slug}/verifying-releases`)}.`, ); } @@ -324,7 +326,8 @@ const pages: Record string | Promise> = { '/privacy': privacy, '/community': communityPage, '/floe/community': floeCommunityPage, - '/verified': verifiedPage, + '/verified': () => verifiedPage('thaw'), + '/floe/verified': () => verifiedPage('floe'), '/built-with': builtWith, [changelogUrl('thaw')]: () => changelog('thaw'), [changelogUrl('floe')]: () => changelog('floe'), diff --git a/lib/site-pages.ts b/lib/site-pages.ts index 79c352d..3f00131 100644 --- a/lib/site-pages.ts +++ b/lib/site-pages.ts @@ -59,6 +59,12 @@ export const sitePages = { title: 'Privacy only works if the security around it does.', shared: 'What SLSA, the OpenSSF badges and the Scorecard are, and where Thaw stands.', }, + floeVerified: { + label: 'Verified', + title: 'Privacy only works if the security around it does.', + shared: 'What SLSA, the OpenSSF badges and the Scorecard are, and where Floe stands.', + product: 'floe' as const, + }, }; export type SitePage = keyof typeof sitePages; diff --git a/lib/verified.test.mjs b/lib/verified.test.mjs index 090375d..e8e242e 100644 --- a/lib/verified.test.mjs +++ b/lib/verified.test.mjs @@ -1,19 +1,22 @@ // The Verified block states what outside bodies say about Thaw. These pin that a value is // never shown under another's name, and that the words beside it follow from the value. import { expect, test } from 'bun:test'; -import { meanings, readDays, standing, verifiedFrom } from './verified.ts'; +import { meaningsFor, readDays, standing, verifiedFrom } from './verified.ts'; const full = { bestPractices: 'gold', baseline: 3, scorecard: 9.1, coverage: 92.3 }; test('every value has an explanation, and every explanation is of a known value', () => { const ids = verifiedFrom(full).map((entry) => entry.id); - expect(ids).toEqual(meanings.map((entry) => entry.id)); + expect(ids).toEqual(meaningsFor('thaw').map((entry) => entry.id)); }); test('a value that could not be read does not shift the others under the wrong name', () => { const values = verifiedFrom({ ...full, bestPractices: undefined, baseline: undefined }); const shown = Object.fromEntries( - meanings.map((entry) => [entry.id, values.find((value) => value.id === entry.id)?.value]), + meaningsFor('thaw').map((entry) => [ + entry.id, + values.find((value) => value.id === entry.id)?.value, + ]), ); expect(shown).toEqual({ slsa: 'Level 3', @@ -35,6 +38,16 @@ test('Floe’s values point at Floe, not at Thaw', () => { expect(values.find((value) => value.id === 'coverage')?.detail).toContain('Floe’s code'); }); +test('Floe’s explanations are of Floe', () => { + const meanings = meaningsFor('floe'); + expect(meanings.map((entry) => entry.id)).toEqual(meaningsFor('thaw').map((entry) => entry.id)); + const about = (id) => meanings.find((entry) => entry.id === id); + expect(about('bestPractices').href).toContain('/15396'); + expect(about('scorecard').href).toContain('github.com/thaw-app/Floe'); + expect(about('coverage').href).toContain('thaw-app_Floe'); + expect(JSON.stringify(meanings)).not.toContain('Thaw'); +}); + test('only the highest badge and the top level are called that', () => { const said = (assurance, id) => verifiedFrom(assurance).find((entry) => entry.id === id)?.detail; expect(said(full, 'bestPractices')).toContain('The highest of three badges'); @@ -66,4 +79,8 @@ test('Thaw is only counted among the few when it holds both marks', () => { ); expect(standing({ field, bestPractices: 'silver', baseline: 3 })).not.toContain('Thaw'); expect(standing({ bestPractices: 'gold', baseline: 3 })).toBe(''); + expect(standing({ field, bestPractices: 'gold', baseline: 3 }, 'floe')).toContain( + 'Floe, a free launcher, is in both groups.', + ); + expect(standing({ field, bestPractices: 'silver', baseline: 3 }, 'floe')).not.toContain('Floe'); }); diff --git a/lib/verified.ts b/lib/verified.ts index 06b5bc8..bb6be9a 100644 --- a/lib/verified.ts +++ b/lib/verified.ts @@ -1,7 +1,5 @@ import { docsRoute, type ProductSlug, products } from '@/lib/shared'; -const repo = products.thaw.repo; - /** A product's entry on OpenSSF's Best Practices list, which holds its badge and Baseline level. */ const bestPracticesUrl = (slug: ProductSlug) => `https://www.bestpractices.dev/projects/${products[slug].bestPractices}`; @@ -10,13 +8,15 @@ const bestPracticesUrl = (slug: ProductSlug) => // for the value, since a score a machine gives and a checklist a project fills in differ. // `id` is what ties one to its value: a value that could not be read is left out of the // list of values, so the two lists cannot be matched by position. -export const meanings: { +export const meaningsFor = ( + slug: ProductSlug, +): { id: VerifiedId; term: string; meaning: string; checkedBy: string; href: string; -}[] = [ +}[] => [ { id: 'slsa', term: 'SLSA build level', @@ -31,7 +31,7 @@ export const meanings: { meaning: 'A checklist from the Open Source Security Foundation for how a project is run, such as how changes get reviewed and how security reports are handled. It has three badges, Passing, Silver and Gold.', checkedBy: 'The project answers each item and publishes its evidence.', - href: bestPracticesUrl('thaw'), + href: bestPracticesUrl(slug), }, { id: 'baseline', @@ -47,14 +47,14 @@ export const meanings: { meaning: 'A tool that inspects the repository itself and scores it out of 10 on things such as whether changes are reviewed, dependencies are pinned and releases are signed.', checkedBy: 'Run by OpenSSF every week. The project cannot edit the score.', - href: `https://scorecard.dev/viewer/?uri=github.com/${repo}`, + href: `https://scorecard.dev/viewer/?uri=github.com/${products[slug].repo}`, }, { id: 'coverage', term: 'Test coverage', - meaning: 'The share of Thaw’s code that its automated tests run.', + meaning: `The share of ${products[slug].name}’s code that its automated tests run.`, checkedBy: 'SonarQube Cloud measures it on every change.', - href: `https://sonarcloud.io/component_measures?id=${repo.replace('/', '_')}&metric=coverage`, + href: `https://sonarcloud.io/component_measures?id=${products[slug].repo.replace('/', '_')}&metric=coverage`, }, ]; @@ -88,14 +88,23 @@ export interface Assurance { const gold = 'var(--verified-gold)'; const green = 'var(--verified-green)'; +// How the page names a product when it counts it among the few. +const described: Record = { + thaw: 'Thaw, a free menu bar app,', + floe: 'Floe, a free launcher,', +}; + /** - * Where those marks put Thaw among other projects, or nothing if the numbers could not be - * read. It only counts Thaw among the few when Thaw holds both marks. + * Where those marks put a product among other projects, or nothing if the numbers could not + * be read. It only counts the product among the few when it holds both marks. */ -export function standing({ field, bestPractices, baseline }: Assurance): string { +export function standing( + { field, bestPractices, baseline }: Assurance, + slug: ProductSlug = 'thaw', +): string { if (!field) return ''; const among = bestPractices === 'gold' && baseline === 3; - return `Of the ${new Intl.NumberFormat('en').format(field.projects)} projects on OpenSSF’s Best Practices list, ${field.gold} hold Gold and ${field.baseline3} meet every Baseline control at Level 3.${among ? ' Thaw, a free menu bar app, is in both groups.' : ''}`; + return `Of the ${new Intl.NumberFormat('en').format(field.projects)} projects on OpenSSF’s Best Practices list, ${field.gold} hold Gold and ${field.baseline3} meet every Baseline control at Level 3.${among ? ` ${described[slug]} is in both groups.` : ''}`; } const badges = ['passing', 'silver', 'gold'];