diff --git a/README.md b/README.md index dcb223e..7c2c563 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 | @@ -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/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 448d9b7..205abc9 100644 --- a/components/assurance.tsx +++ b/components/assurance.tsx @@ -3,28 +3,39 @@ 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 - * verified about its releases, and how many people use and build it. /verified explains - * each one. + * 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. Its Verified page + * explains each one. */ -export function Assurance() { +export function VerifiedBlock({ + values, + numbers, + communityHref, + meaningsHref, +}: { + values: Verified[]; + numbers: ReturnType; + communityHref: string; + meaningsHref: string; +}) { + const stepped = values.length === 5; return ( <> @@ -36,7 +47,7 @@ export function Assurance() {
    - {verified.map((entry, index) => ( + {values.map((entry, index) => (
  • - + What does this mean? - + Community

    @@ -94,3 +105,14 @@ export function Assurance() { ); } + +export function Assurance() { + return ( + + ); +} diff --git a/components/floe-parts.tsx b/components/floe-parts.tsx index b8e73bb..76443ce 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,14 @@ 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/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/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 4a02496..2cb9095 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,12 +67,73 @@ 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, 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(); + 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', + '/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 }) => { for (const path of [ '/', '/floe', '/floe/roadmap', '/floe/community', + '/floe/verified', '/community', '/verified', '/roadmap', @@ -384,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/floe-community.json b/lib/floe-community.json index f7462d8..9289979 100644 --- a/lib/floe-community.json +++ b/lib/floe-community.json @@ -1,26 +1,28 @@ { "stars": 167, - "downloads": 297, - "discord": 384, - "contributors": 4, + "downloads": 311, + "discord": 385, + "contributors": 5, "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", @@ -32,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", @@ -46,17 +53,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 +199,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/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/project.json b/lib/project.json index 57a8d07..0c9cdb5 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": "development", + "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/site-markdown.ts b/lib/site-markdown.ts index 6c5b2e6..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 @@ -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', '/floe/verified')}.`, ); } @@ -260,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}` : ''}`, @@ -275,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`)}.`, ); } @@ -319,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 74236f1..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', @@ -24,6 +27,27 @@ 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('/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'); + expect(href('coverage')).toContain('thaw-app_Floe'); + 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'); @@ -55,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 a7f56a0..bb6be9a 100644 --- a/lib/verified.ts +++ b/lib/verified.ts @@ -1,18 +1,22 @@ -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}`; // 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 // 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', @@ -27,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: links.bestPractices, + href: bestPracticesUrl(slug), }, { id: 'baseline', @@ -43,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`, }, ]; @@ -84,26 +88,36 @@ 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']; 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 +129,7 @@ export function verifiedFrom(assurance: Assurance): Verified[] { steps: 3, reached: 3, tone: green, - href: `${docsRoute}/thaw/verifying-releases`, + href: `${docsRoute}/${slug}/verifying-releases`, }, badge >= 0 && typeof bestPractices === 'string' && { @@ -126,7 +140,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 +153,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 +168,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/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 1060071..15f52c5 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 @@ -185,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.