From bf24e0cb415cb7d471a3fdb7d36adbd26ae59844 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:17:50 +0200 Subject: [PATCH 1/3] chore: add a formatter, matching the style this repo already writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit singleQuote=true was chosen by counting this repo's own imports, not by fleet decree. The fleet is genuinely split and the two repos that already had a .prettierrc disagreed with each other, so there was no standard to restore. Quote style does not cross repo boundaries; having a gate does. Markdown is ignored for now — prettier rewraps prose, which would bury the real diff. Co-Authored-By: Claude Opus 5 --- .prettierignore | 31 +++++++++++++++++ .prettierrc | 9 +++++ package-lock.json | 86 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 ++- 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b698663 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,31 @@ +# Build output and vendored trees — formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Generated during a build, so it is absent locally and present in CI — which +# makes a clean local --check no evidence at all. Contentlayer's output also +# uses import assertions, which prettier's parser rejects outright. +.contentlayer +.astro +.svelte-kit +storybook-static +test-results +playwright-report + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..616247a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/package-lock.json b/package-lock.json index ff86837..842a99c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,13 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", + "prettier": "3.9.6", "tailwindcss": "^4", "tsx": "^4.23.12", "typescript": "^5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { @@ -2004,6 +2008,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -6114,6 +6184,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 8e74ea4..88b3c49 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "lint": "eslint .", "type-check": "tsc --noEmit", "test": "tsx --test test/*.test.ts", - "verify": "npm run type-check && npm run lint && npm run test" + "verify": "npm run format:check && npm run type-check && npm run lint && npm run test", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "next": "16.2.4", @@ -27,6 +29,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", + "prettier": "3.9.6", "tailwindcss": "^4", "tsx": "^4.23.12", "typescript": "^5" From 59f014a8ac427bd3bfc62aa8fad2bc225b9390c4 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:18:02 +0200 Subject: [PATCH 2/3] style: format with prettier (9 files) Mechanical. No behaviour change. This SHA is listed in .git-blame-ignore-revs so `git blame` skips it. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 4 +- app/[[...path]]/page.tsx | 9 +- config/site-substrata.ts | 58 +++++---- config/substrata-acting.ts | 4 +- config/substrata-coverage.ts | 48 +++---- config/substrata-participants.ts | 214 +++++++++++++++---------------- config/substrata.ts | 4 +- test/site-content.test.ts | 2 +- tsconfig.json | 14 +- 9 files changed, 177 insertions(+), 180 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a646416..554fa37 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ version: 2 updates: - package-ecosystem: npm - directory: "/" + directory: '/' schedule: interval: weekly open-pull-requests-limit: 5 @@ -20,7 +20,7 @@ updates: update-types: [minor, patch] - package-ecosystem: github-actions - directory: "/" + directory: '/' schedule: interval: weekly open-pull-requests-limit: 3 diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 4373972..7096b85 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -21,7 +21,7 @@ interface RouteParams { * adding one is a config entry, never a new route file. */ export function generateStaticParams(): Array<{ path?: string[] }> { - return sitePages().map(page => ({ path: page.path ? [page.path] : [] })); + return sitePages().map((page) => ({ path: page.path ? [page.path] : [] })); } export async function generateMetadata({ params }: RouteParams): Promise { @@ -45,7 +45,12 @@ export default async function Page({ params }: RouteParams) { return (
- +
{!pageRendersOwnHeader(page) && ( diff --git a/config/site-substrata.ts b/config/site-substrata.ts index debdc20..c3f4990 100644 --- a/config/site-substrata.ts +++ b/config/site-substrata.ts @@ -56,7 +56,7 @@ import type { SiteChrome, SitePage, SiteSection } from './site-content'; import { SITE } from '../lib/site'; const ROLE_LABEL: Record = Object.fromEntries( - PRODUCER_ROLES.map(role => [role.id, role.label]) + PRODUCER_ROLES.map((role) => [role.id, role.label]), ); export function substrataSiteChrome(): SiteChrome { @@ -77,8 +77,8 @@ export function substrataSiteChrome(): SiteChrome { function homePage(): SitePage { const progress = coverageProgress(); - const activePhases = PHASES.filter(phase => phase.status === 'active'); - const companies = new Set(COVERAGE.flatMap(entry => entry.producers.map(p => p.name))).size; + const activePhases = PHASES.filter((phase) => phase.status === 'active'); + const companies = new Set(COVERAGE.flatMap((entry) => entry.producers.map((p) => p.name))).size; return { path: '', @@ -136,7 +136,7 @@ function homePage(): SitePage { 'A node enters coverage, or the book, only if it passes both. Failing either is a ' + 'decline — and we decline every week.', columns: 3, - cards: MANDATE_CURVES.map(curve => ({ + cards: MANDATE_CURVES.map((curve) => ({ title: curve.label, body: curve.detail, meta: curve.test, @@ -146,7 +146,7 @@ function homePage(): SitePage { kind: 'definitions', heading: 'The chokepoint screen', blurb: EXCLUSION_RULE.rule, - items: CHOKEPOINT_TEST.map(factor => ({ + items: CHOKEPOINT_TEST.map((factor) => ({ term: factor.question, detail: factor.detail, })), @@ -157,7 +157,7 @@ function homePage(): SitePage { blurb: 'Two phases at once. A desk is not one of them — see Disclosure for what this ' + 'firm does and does not do.', - items: activePhases.map(phase => ({ term: phase.label, detail: phase.detail })), + items: activePhases.map((phase) => ({ term: phase.label, detail: phase.detail })), }, ], }; @@ -179,7 +179,7 @@ function mandatePage(): SitePage { kind: 'cards', heading: 'Three curves', columns: 3, - cards: MANDATE_CURVES.map(curve => ({ + cards: MANDATE_CURVES.map((curve) => ({ title: curve.label, body: curve.detail, meta: curve.test, @@ -191,7 +191,7 @@ function mandatePage(): SitePage { blurb: 'Being on a curve is not enough. Most of a supply chain is substitutable, and ' + 'therefore uninteresting. A node earns coverage when it gates a curve.', - items: CHOKEPOINT_TEST.map(factor => ({ term: factor.question, detail: factor.detail })), + items: CHOKEPOINT_TEST.map((factor) => ({ term: factor.question, detail: factor.detail })), }, { kind: 'cards', @@ -201,14 +201,14 @@ function mandatePage(): SitePage { 'reaches robotics, AI hardware and additive manufacturing without becoming ' + '“everything”: you arrive at them by tracing a chain you were already mapping.', columns: 3, - cards: NODE_TYPES.map(node => ({ title: node.label, body: node.detail })), + cards: NODE_TYPES.map((node) => ({ title: node.label, body: node.detail })), }, { kind: 'definitions', heading: 'Sequence', blurb: 'Research first, because it costs nothing but attention and it is the sourcing work the desk needs anyway.', - items: PHASES.map(phase => ({ + items: PHASES.map((phase) => ({ term: `${phase.label}${phase.status === 'active' ? ' — active' : ''}`, detail: phase.detail, })), @@ -235,9 +235,9 @@ function mapPage(): SitePage { // The material's own research — why it gates, and which grade actually ships // — used to live on the desk page. With no desk there is no desk page, and // that content belongs here anyway: it is research, not a product listing. - const materialById = new Map(MATERIALS.map(material => [material.title, material])); + const materialById = new Map(MATERIALS.map((material) => [material.title, material])); - const materialTables: SiteSection[] = COVERAGE.map(entry => { + const materialTables: SiteSection[] = COVERAGE.map((entry) => { const material = materialById.get(entry.material); return { kind: 'table' as const, @@ -251,7 +251,7 @@ function mapPage(): SitePage { // across a row — mono keeps them aligned and comparable. monoColumns: [1], statusColumn: 3, - rows: entry.producers.map(producer => [ + rows: entry.producers.map((producer) => [ producer.name, producer.jurisdictions.join(' '), ROLE_LABEL[producer.role] ?? producer.role, @@ -294,7 +294,7 @@ function mapPage(): SitePage { kind: 'index', heading: 'Materials', blurb: 'Fifteen chokepoints. The number beside each is how many producers are mapped.', - entries: COVERAGE.map(entry => ({ + entries: COVERAGE.map((entry) => ({ label: entry.material, meta: String(entry.producers.length), anchor: materialAnchor(entry.material), @@ -315,12 +315,12 @@ function chokepointsPage(): SitePage { // Cards, not a table. A table of names and country codes scans nicely and // says nothing — the claim IS the reason it gates, and a research page that // hides its reasoning behind a tidy grid has published a list, not research. - const byCurve = MANDATE_CURVES.map(curve => ({ + const byCurve = MANDATE_CURVES.map((curve) => ({ kind: 'cards' as const, heading: curve.label, blurb: curve.detail, columns: 2 as const, - cards: CHOKEPOINTS.filter(point => point.curve === curve.id).map(point => ({ + cards: CHOKEPOINTS.filter((point) => point.curve === curve.id).map((point) => ({ title: point.name, body: point.why, meta: [ @@ -362,7 +362,7 @@ function chokepointsPage(): SitePage { { label: 'Non-material chokepoints', value: String(progress.total) }, { label: 'Kinds of node', - value: String(new Set(CHOKEPOINTS.map(point => point.type)).size), + value: String(new Set(CHOKEPOINTS.map((point) => point.type)).size), note: 'Machines, processes, companies and people.', }, ], @@ -404,7 +404,7 @@ function thesisPage(): SitePage { 'circumstances — see Acting on it for what that does and does not permit.', ], }, - ...INVESTMENT_THESIS.map(claim => ({ + ...INVESTMENT_THESIS.map((claim) => ({ kind: 'definitions' as const, heading: claim.claim, blurb: claim.detail, @@ -422,7 +422,7 @@ function participantsPage(): SitePage { const progress = participantProgress(); const binding = bindingParticipants(); - const layerTables: SiteSection[] = CHAIN_LAYERS.flatMap(layer => { + const layerTables: SiteSection[] = CHAIN_LAYERS.flatMap((layer) => { const rows = participantsInLayer(layer.id); if (rows.length === 0) { return []; @@ -436,7 +436,7 @@ function participantsPage(): SitePage { columns: ['Participant', 'Role in the chain', 'Where', 'Scarcity'], monoColumns: [2], statusColumn: 3, - rows: rows.map(item => [ + rows: rows.map((item) => [ item.name, item.role, item.jurisdictions.join(' '), @@ -493,10 +493,12 @@ function participantsPage(): SitePage { blurb: 'The mandate’s own screen — concentration, substitutability, lead time, demand ' + 'inelasticity — applied one participant at a time.', - items: (Object.keys(SCARCITY_DETAIL) as Array).map(grade => ({ - term: SCARCITY_LABEL[grade], - detail: SCARCITY_DETAIL[grade], - })), + items: (Object.keys(SCARCITY_DETAIL) as Array).map( + (grade) => ({ + term: SCARCITY_LABEL[grade], + detail: SCARCITY_DETAIL[grade], + }), + ), }, { kind: 'cards', @@ -505,7 +507,7 @@ function participantsPage(): SitePage { `${binding.length} of ${progress.total} participants are graded a hard constraint. ` + 'These are the ones worth knowing by name.', columns: 2, - cards: binding.map(item => ({ + cards: binding.map((item) => ({ title: item.name, body: item.why, meta: `${item.role} · ${item.jurisdictions.join(' ')}`, @@ -515,7 +517,7 @@ function participantsPage(): SitePage { kind: 'index', heading: 'The chain, layer by layer', blurb: 'Ordered upstream to downstream. The number is how many participants are mapped.', - entries: CHAIN_LAYERS.map(layer => ({ + entries: CHAIN_LAYERS.map((layer) => ({ label: layer.name, meta: String(participantsInLayer(layer.id).length), anchor: layer.id, @@ -551,7 +553,7 @@ function actingPage(): SitePage { detail: limit, })), }, - ...ACTION_ROUTES.map(route => ({ + ...ACTION_ROUTES.map((route) => ({ kind: 'definitions' as const, heading: route.name, blurb: route.gives, @@ -593,7 +595,7 @@ function actingPage(): SitePage { // the status belongs next to the requirement, not in a column of its own. kind: 'definitions', heading: 'The ledger', - items: READINESS.map(item => ({ + items: READINESS.map((item) => ({ term: `${item.requirement} — ${READINESS_STATUS_LABEL[item.status]}`, detail: item.detail, })), diff --git a/config/substrata-acting.ts b/config/substrata-acting.ts index 76cff79..4dd591e 100644 --- a/config/substrata-acting.ts +++ b/config/substrata-acting.ts @@ -349,8 +349,8 @@ export interface ReadinessProgress { export function readinessProgress(): ReadinessProgress { return { total: READINESS.length, - done: READINESS.filter(item => item.status === 'done').length, - inProgress: READINESS.filter(item => item.status === 'in-progress').length, + done: READINESS.filter((item) => item.status === 'done').length, + inProgress: READINESS.filter((item) => item.status === 'in-progress').length, }; } diff --git a/config/substrata-coverage.ts b/config/substrata-coverage.ts index 1e23550..1f5a361 100644 --- a/config/substrata-coverage.ts +++ b/config/substrata-coverage.ts @@ -315,22 +315,22 @@ export interface CoverageProgress { * a universe of 90 unsourced entries is 0% covered, not 100% mapped. */ export function coverageProgress(): CoverageProgress { - const rows = COVERAGE.flatMap(entry => entry.producers); - const covered = new Set(COVERAGE.map(entry => entry.material)); + const rows = COVERAGE.flatMap((entry) => entry.producers); + const covered = new Set(COVERAGE.map((entry) => entry.material)); return { total: rows.length, - sourced: rows.filter(producer => producer.source !== null).length, - uncoveredMaterials: MATERIALS.map(material => material.title).filter( - title => !covered.has(title) + sourced: rows.filter((producer) => producer.source !== null).length, + uncoveredMaterials: MATERIALS.map((material) => material.title).filter( + (title) => !covered.has(title), ), }; } /** @returns every material a company appears on — the overlaps are the point. */ export function materialsFor(companyName: string): string[] { - return COVERAGE.filter(entry => - entry.producers.some(producer => producer.name === companyName) - ).map(entry => entry.material); + return COVERAGE.filter((entry) => + entry.producers.some((producer) => producer.name === companyName), + ).map((entry) => entry.material); } // ===================================================================== @@ -364,7 +364,7 @@ function node( type: NodeType, curve: CurveId, jurisdictions: string[], - why: string + why: string, ): Chokepoint { return { name, type, curve, jurisdictions, why, source: null }; } @@ -385,49 +385,49 @@ export const CHOKEPOINTS: readonly Chokepoint[] = [ 'machine', 'compute-per-joule', ['NL'], - 'One company on earth builds them, the queue is measured in years, and no second source is in progress. Every leading-edge wafer in the world is downstream of one factory.' + 'One company on earth builds them, the queue is measured in years, and no second source is in progress. Every leading-edge wafer in the world is downstream of one factory.', ), node( 'EUV projection optics', 'process', 'compute-per-joule', ['DE'], - 'The mirror systems inside the scanner are polished to a tolerance one supplier has ever achieved. It is a chokepoint inside a chokepoint, and the constraint is know-how, not capacity.' + 'The mirror systems inside the scanner are polished to a tolerance one supplier has ever achieved. It is a chokepoint inside a chokepoint, and the constraint is know-how, not capacity.', ), node( 'Advanced packaging capacity', 'process', 'compute-per-joule', ['TW', 'KR', 'US'], - 'Accelerator output is gated by how many dies can be packaged onto an interposer, not by wafer starts. Capacity is allocated years ahead, which makes the allocation itself the scarce good.' + 'Accelerator output is gated by how many dies can be packaged onto an interposer, not by wafer starts. Capacity is allocated years ahead, which makes the allocation itself the scarce good.', ), node( 'High-bandwidth memory stacking yield', 'process', 'compute-per-joule', ['KR', 'US'], - 'Three suppliers, and the yield on stacking and bonding is knowledge that does not transfer when someone else buys the same equipment.' + 'Three suppliers, and the yield on stacking and bonding is knowledge that does not transfer when someone else buys the same equipment.', ), node( 'Leading-edge foundry capacity', 'company', 'compute-per-joule', ['TW', 'KR', 'US'], - 'A handful of fabs can run the newest node at volume. New capacity is a multi-year, multi-billion commitment, so the supply curve cannot answer a demand shock.' + 'A handful of fabs can run the newest node at volume. New capacity is a multi-year, multi-billion commitment, so the supply curve cannot answer a demand shock.', ), node( 'Photoresist formulation', 'process', 'compute-per-joule', ['JP'], - 'The chemistry is qualified per process per fab and is overwhelmingly Japanese. Substituting a resist is a re-qualification programme, not a purchase.' + 'The chemistry is qualified per process per fab and is overwhelmingly Japanese. Substituting a resist is a re-qualification programme, not a purchase.', ), node( 'Semiconductor process engineers', 'person', 'compute-per-joule', [], - 'The constraint nobody can buy. A fab ramp moves at the speed of people who have done it before, which is why capacity announcements and capacity slip on different clocks.' + 'The constraint nobody can buy. A fab ramp moves at the speed of people who have done it before, which is why capacity announcements and capacity slip on different clocks.', ), // ---------- Joules delivered ---------- @@ -436,28 +436,28 @@ export const CHOKEPOINTS: readonly Chokepoint[] = [ 'machine', 'joules-delivered', ['KR', 'DE', 'JP', 'US'], - 'Lead times run to several years, and a datacentre cannot be energised without one. This gates more announced compute today than chip supply does.' + 'Lead times run to several years, and a datacentre cannot be energised without one. This gates more announced compute today than chip supply does.', ), node( 'Grid interconnection queues', 'process', 'joules-delivered', [], - 'Not a material, not a machine, and frequently the binding constraint: a multi-year administrative queue between a signed site and a live megawatt.' + 'Not a material, not a machine, and frequently the binding constraint: a multi-year administrative queue between a signed site and a live megawatt.', ), node( 'Heavy-duty gas turbine order books', 'machine', 'joules-delivered', ['US', 'DE'], - 'The fastest route to firm power at scale, and the order books are effectively sold out. A slot is worth more than the turbine price implies.' + 'The fastest route to firm power at scale, and the order books are effectively sold out. A slot is worth more than the turbine price implies.', ), node( 'High-voltage cable and switchgear', 'machine', 'joules-delivered', ['DE', 'IT', 'KR'], - 'The unglamorous half of energisation. Same multi-year lead times as transformers, same inability to respond quickly to a demand shock.' + 'The unglamorous half of energisation. Same multi-year lead times as transformers, same inability to respond quickly to a demand shock.', ), // ---------- Actuation ---------- @@ -466,21 +466,21 @@ export const CHOKEPOINTS: readonly Chokepoint[] = [ 'process', 'actuation', ['CN'], - 'Even where the metal is mined elsewhere, sintering and grain-boundary diffusion are concentrated in one jurisdiction. The chokepoint sits downstream of the mine, where most coverage is not looking.' + 'Even where the metal is mined elsewhere, sintering and grain-boundary diffusion are concentrated in one jurisdiction. The chokepoint sits downstream of the mine, where most coverage is not looking.', ), node( 'Precision reduction drives', 'company', 'actuation', ['JP'], - 'Harmonic and cycloidal drives set what a robot joint can do. Few qualified suppliers, and the tolerances are decades of accumulated manufacturing practice.' + 'Harmonic and cycloidal drives set what a robot joint can do. Few qualified suppliers, and the tolerances are decades of accumulated manufacturing practice.', ), node( 'Robot-grade encoders and force sensors', 'company', 'actuation', ['JP', 'DE'], - 'Closing the loop is what separates a manipulator from an arm. Narrow supply, and qualification is per-application.' + 'Closing the loop is what separates a manipulator from an arm. Narrow supply, and qualification is per-application.', ), ]; @@ -498,7 +498,7 @@ export function chokepointProgress(): ChokepointProgress { } return { total: CHOKEPOINTS.length, - sourced: CHOKEPOINTS.filter(point => point.source !== null).length, + sourced: CHOKEPOINTS.filter((point) => point.source !== null).length, byCurve, }; } diff --git a/config/substrata-participants.ts b/config/substrata-participants.ts index 228ce8c..46694b8 100644 --- a/config/substrata-participants.ts +++ b/config/substrata-participants.ts @@ -170,7 +170,7 @@ function p( jurisdictions: string[], role: string, scarcity: ScarcityGrade, - why: string + why: string, ): Participant { return { name, layer, jurisdictions, role, scarcity, why, source: null }; } @@ -187,7 +187,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['NO', 'US'], 'High-purity quartz sand', 'chokepoint', - 'Inner-layer crucible quartz comes in practice from a very small number of deposits, and every Czochralski puller on earth needs it.' + 'Inner-layer crucible quartz comes in practice from a very small number of deposits, and every Czochralski puller on earth needs it.', ), p( 'Sibelco', @@ -195,7 +195,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['BE', 'US'], 'High-purity quartz sand', 'chokepoint', - 'The other holder of the same rare deposit quality. Two names deep is the whole of the upstream for this input.' + 'The other holder of the same rare deposit quality. Two names deep is the whole of the upstream for this input.', ), p( 'Sibanye-Stillwater', @@ -203,7 +203,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['ZA'], 'PGM mining incl. ruthenium', 'concentrated', - 'Ruthenium is a by-product, so its supply is set by platinum economics rather than by demand for it.' + 'Ruthenium is a by-product, so its supply is set by platinum economics rather than by demand for it.', ), p( 'Impala Platinum', @@ -211,7 +211,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['ZA'], 'PGM mining', 'concentrated', - 'Same by-product logic, same narrow geography.' + 'Same by-product logic, same narrow geography.', ), p( 'Nornickel', @@ -219,7 +219,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['RU'], 'Nickel and PGM mining', 'concentrated', - 'Material share of world PGM and nickel, with sanctions risk layered on top of geology.' + 'Material share of world PGM and nickel, with sanctions risk layered on top of geology.', ), p( 'MP Materials', @@ -227,7 +227,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Rare-earth ore', 'concentrated', - 'The main non-Chinese light rare-earth mine. Mining diversified before separation did, which is the gap the map cares about.' + 'The main non-Chinese light rare-earth mine. Mining diversified before separation did, which is the gap the map cares about.', ), p( 'Lynas Rare Earths', @@ -235,7 +235,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['AU', 'MY'], 'Rare-earth mining and separation', 'concentrated', - 'The most complete non-Chinese rare-earth chain, and still small against the incumbent.' + 'The most complete non-Chinese rare-earth chain, and still small against the incumbent.', ), p( 'Yunnan Tin', @@ -243,7 +243,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CN'], 'Tin mining and smelting', 'concentrated', - 'Large in tin metal; the EUV-grade constraint sits downstream of it.' + 'Large in tin metal; the EUV-grade constraint sits downstream of it.', ), p( 'Minsur', @@ -251,7 +251,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['PE'], 'Tin mining and smelting', 'competitive', - 'Tin metal has several credible producers. It is purity, not tonnage, that binds here.' + 'Tin metal has several credible producers. It is purity, not tonnage, that binds here.', ), p( 'PT Timah', @@ -259,7 +259,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['ID'], 'Tin mining and smelting', 'competitive', - 'Same: the scarcity in this chain is an upgrading step, not an ore body.' + 'Same: the scarcity in this chain is an upgrading step, not an ore body.', ), p( 'QatarEnergy', @@ -267,7 +267,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['QA'], 'Helium from LNG', 'concentrated', - 'Helium exists commercially only as a by-product of a few gas fields with unusual composition.' + 'Helium exists commercially only as a by-product of a few gas fields with unusual composition.', ), p( 'ExxonMobil', @@ -275,7 +275,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Helium from natural gas', 'concentrated', - 'One of a very small number of fields worldwide rich enough to justify extraction.' + 'One of a very small number of fields worldwide rich enough to justify extraction.', ), // ---------------- Refining & separation ---------------- @@ -285,7 +285,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE', 'US'], 'Electronic-grade polysilicon', 'chokepoint', - 'Solar-grade polysilicon has many producers; electronic-grade has very few, and the gap is orders of magnitude of impurity.' + 'Solar-grade polysilicon has many producers; electronic-grade has very few, and the gap is orders of magnitude of impurity.', ), p( 'Hemlock Semiconductor', @@ -293,7 +293,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Electronic-grade polysilicon', 'chokepoint', - 'One of a handful of qualified suppliers of the first material in the entire chain.' + 'One of a handful of qualified suppliers of the first material in the entire chain.', ), p( 'Tokuyama', @@ -301,7 +301,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP', 'MY'], 'Electronic-grade polysilicon', 'chokepoint', - 'Same short list, different geography — which is most of why it matters.' + 'Same short list, different geography — which is most of why it matters.', ), p( 'OCI', @@ -309,7 +309,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR', 'MY'], 'Polysilicon', 'concentrated', - 'Credible at scale, with electronic-grade a narrower qualification than volume implies.' + 'Credible at scale, with electronic-grade a narrower qualification than volume implies.', ), p( 'China Northern Rare Earth', @@ -317,7 +317,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CN'], 'Rare-earth separation', 'chokepoint', - 'Separation, not mining, is where the rare-earth chain actually narrows, and it narrows here.' + 'Separation, not mining, is where the rare-earth chain actually narrows, and it narrows here.', ), p( 'Shenghe Resources', @@ -325,7 +325,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CN'], 'Rare-earth separation and trading', 'chokepoint', - 'Processes feedstock from mines all over the world, including ones marketed as diversification.' + 'Processes feedstock from mines all over the world, including ones marketed as diversification.', ), p( 'Chinalco', @@ -333,7 +333,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CN'], 'Gallium from alumina refining', 'chokepoint', - 'Gallium is an alumina by-product, so supply cannot answer price — and it is export-controlled.' + 'Gallium is an alumina by-product, so supply cannot answer price — and it is export-controlled.', ), p( 'Heraeus', @@ -341,7 +341,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE'], 'Precious-metal refining', 'concentrated', - 'One of the few refiners able to deliver PGMs at semiconductor purity.' + 'One of the few refiners able to deliver PGMs at semiconductor purity.', ), p( 'Johnson Matthey', @@ -349,7 +349,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['GB'], 'PGM refining', 'concentrated', - 'Long-established PGM chemistry; a short list of peers worldwide.' + 'Long-established PGM chemistry; a short list of peers worldwide.', ), p( 'Umicore', @@ -357,7 +357,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['BE'], 'PGM refining and recycling', 'concentrated', - 'Secondary supply is often the only elastic source in these metals.' + 'Secondary supply is often the only elastic source in these metals.', ), p( 'Linde', @@ -365,7 +365,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['GB', 'US', 'DE'], 'Industrial and electronic gases', 'concentrated', - 'Noble gases come from air separation attached to heavy industry, which limits where they can come from at all.' + 'Noble gases come from air separation attached to heavy industry, which limits where they can come from at all.', ), p( 'Air Liquide', @@ -373,7 +373,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['FR'], 'Industrial and electronic gases', 'concentrated', - 'One of three global gas majors; the fab-qualified end is narrower than the industrial one.' + 'One of three global gas majors; the fab-qualified end is narrower than the industrial one.', ), p( 'Air Products', @@ -381,7 +381,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Industrial gases and helium', 'concentrated', - 'Helium distribution is a small club with long-dated source contracts.' + 'Helium distribution is a small club with long-dated source contracts.', ), p( 'Iceblick', @@ -389,7 +389,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['UA'], 'Neon and rare gases', 'concentrated', - 'The 2022 squeeze made the point: an industrial-gas map and a war map turned out to be the same map.' + 'The 2022 squeeze made the point: an industrial-gas map and a war map turned out to be the same map.', ), p( '5N Plus', @@ -397,7 +397,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CA', 'DE'], 'High-purity specialty metals', 'concentrated', - 'Upgrading to five nines and beyond is a different business from producing the metal.' + 'Upgrading to five nines and beyond is a different business from producing the metal.', ), // ---------------- Conversion ---------------- @@ -407,7 +407,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], '300 mm prime silicon wafers', 'chokepoint', - 'Five firms supply essentially all prime 300 mm capacity, and qualification at a leading-edge fab takes years.' + 'Five firms supply essentially all prime 300 mm capacity, and qualification at a leading-edge fab takes years.', ), p( 'SUMCO', @@ -415,7 +415,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], '300 mm prime silicon wafers', 'chokepoint', - 'The other half of a duopoly at the top of the wafer market.' + 'The other half of a duopoly at the top of the wafer market.', ), p( 'GlobalWafers', @@ -423,7 +423,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['TW'], 'Silicon wafers', 'concentrated', - 'Third of the big five, with a genuine multi-region footprint.' + 'Third of the big five, with a genuine multi-region footprint.', ), p( 'Siltronic', @@ -431,7 +431,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE'], 'Silicon wafers', 'concentrated', - 'European supply of an input with almost no European alternative.' + 'European supply of an input with almost no European alternative.', ), p( 'SK Siltron', @@ -439,7 +439,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR'], 'Silicon and SiC wafers', 'concentrated', - 'Captive-adjacent to Korean memory, and one of few SiC entrants at scale.' + 'Captive-adjacent to Korean memory, and one of few SiC entrants at scale.', ), p( 'Momentive Technologies', @@ -447,7 +447,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Fused quartz crucibles', 'chokepoint', - 'Turning rare sand into a crucible that survives a pull is knowledge held in very few places.' + 'Turning rare sand into a crucible that survives a pull is knowledge held in very few places.', ), p( 'Shin-Etsu Quartz', @@ -455,7 +455,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Fused quartz components', 'chokepoint', - 'Same step, same shortness of the list.' + 'Same step, same shortness of the list.', ), p( 'Ferrotec', @@ -463,7 +463,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP', 'CN'], 'Quartz and fab consumables', 'concentrated', - 'Broad consumables base spanning both sides of an export-control line.' + 'Broad consumables base spanning both sides of an export-control line.', ), p( 'Element Six', @@ -471,7 +471,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['GB', 'IE'], 'CVD synthetic diamond', 'concentrated', - 'Reactor time, not raw material, is the constraint on optical-grade diamond.' + 'Reactor time, not raw material, is the constraint on optical-grade diamond.', ), p( 'Coherent', @@ -479,7 +479,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'SiC substrates, diamond, photonics', 'concentrated', - 'One of the few firms present in several of this chain’s narrow materials at once.' + 'One of the few firms present in several of this chain’s narrow materials at once.', ), p( 'Wolfspeed', @@ -487,7 +487,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Silicon carbide substrates', 'concentrated', - 'The 150 to 200 mm transition resets everyone’s yield curve, which is where the scarcity currently lives.' + 'The 150 to 200 mm transition resets everyone’s yield curve, which is where the scarcity currently lives.', ), p( 'Resonac', @@ -495,7 +495,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'SiC epitaxy and fab materials', 'concentrated', - 'Deep in the materials nobody outside the industry can name.' + 'Deep in the materials nobody outside the industry can name.', ), p( 'Fujikura', @@ -503,7 +503,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'REBCO superconducting tape', 'concentrated', - 'A single high-field magnet consumes tape by the kilometre against a small world output.' + 'A single high-field magnet consumes tape by the kilometre against a small world output.', ), p( 'Faraday Factory Japan', @@ -511,7 +511,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'REBCO superconducting tape', 'concentrated', - 'One of the very few able to ship fusion-programme quantities at all.' + 'One of the very few able to ship fusion-programme quantities at all.', ), p( 'Neo Performance Materials', @@ -519,7 +519,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CA', 'EE'], 'Rare-earth magnets and materials', 'concentrated', - 'The main non-Chinese magnet-making capacity outside Japan, and small against demand.' + 'The main non-Chinese magnet-making capacity outside Japan, and small against demand.', ), p( 'Less Common Metals', @@ -527,7 +527,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['GB'], 'Rare-earth alloys and strip', 'chokepoint', - 'A tiny specialist standing between Western separated oxide and a finished magnet.' + 'A tiny specialist standing between Western separated oxide and a finished magnet.', ), p( 'Nippon Steel', @@ -535,7 +535,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Grain-oriented electrical steel', 'concentrated', - 'Transformer cores are made on a small number of qualified lines worldwide.' + 'Transformer cores are made on a small number of qualified lines worldwide.', ), p( 'POSCO', @@ -543,7 +543,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR'], 'Grain-oriented electrical steel', 'concentrated', - 'Same short list, and the same multi-year lead times downstream.' + 'Same short list, and the same multi-year lead times downstream.', ), p( 'Indium Corporation', @@ -551,7 +551,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'High-purity metals and solders', 'concentrated', - 'Seven-nines upgrading is a specialist step with few qualified providers.' + 'Seven-nines upgrading is a specialist step with few qualified providers.', ), // ---------------- Equipment & consumables ---------------- @@ -561,7 +561,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['NL'], 'EUV and DUV lithography systems', 'chokepoint', - 'One company on earth builds EUV, the queue runs to years, and no second source is in progress.' + 'One company on earth builds EUV, the queue runs to years, and no second source is in progress.', ), p( 'Carl Zeiss SMT', @@ -569,7 +569,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE'], 'EUV projection optics', 'chokepoint', - 'A chokepoint inside a chokepoint: the mirrors are polished to a tolerance one supplier has ever achieved.' + 'A chokepoint inside a chokepoint: the mirrors are polished to a tolerance one supplier has ever achieved.', ), p( 'Trumpf', @@ -577,7 +577,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE'], 'EUV plasma-source lasers', 'chokepoint', - 'The drive laser is as single-sourced as the scanner it sits inside.' + 'The drive laser is as single-sourced as the scanner it sits inside.', ), p( 'Applied Materials', @@ -585,7 +585,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Deposition, etch and process tools', 'concentrated', - 'Broadest tool portfolio, with several steps where it is effectively the only qualified option.' + 'Broadest tool portfolio, with several steps where it is effectively the only qualified option.', ), p( 'Lam Research', @@ -593,7 +593,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Etch and deposition', 'concentrated', - 'High-aspect-ratio etch for 3D memory is a narrow specialism.' + 'High-aspect-ratio etch for 3D memory is a narrow specialism.', ), p( 'Tokyo Electron', @@ -601,7 +601,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Coaters, developers, etch', 'concentrated', - 'Track systems pair with lithography and are qualified alongside it.' + 'Track systems pair with lithography and are qualified alongside it.', ), p( 'KLA', @@ -609,7 +609,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Process control and metrology', 'concentrated', - 'You cannot yield what you cannot measure, and few can measure at this scale.' + 'You cannot yield what you cannot measure, and few can measure at this scale.', ), p( 'ASM International', @@ -617,7 +617,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['NL'], 'Atomic layer deposition', 'concentrated', - 'ALD became unavoidable as devices went vertical, on a short supplier list.' + 'ALD became unavoidable as devices went vertical, on a short supplier list.', ), p( 'JSR', @@ -625,7 +625,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Photoresists', 'chokepoint', - 'Resist chemistry is qualified per process per fab; substituting one is a programme, not a purchase.' + 'Resist chemistry is qualified per process per fab; substituting one is a programme, not a purchase.', ), p( 'Tokyo Ohka Kogyo', @@ -633,7 +633,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Photoresists and process chemicals', 'chokepoint', - 'The same Japanese concentration that made resist an export-control talking point.' + 'The same Japanese concentration that made resist an export-control talking point.', ), p( 'Shin-Etsu Chemical', @@ -641,7 +641,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Photoresists, masks and silicones', 'chokepoint', - 'Present at several narrow points of this chain simultaneously.' + 'Present at several narrow points of this chain simultaneously.', ), p( 'Nikon', @@ -649,7 +649,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Lithography systems', 'competitive', - 'Credible in mature-node lithography, and not a factor at the leading edge — which is what "competitive" means here.' + 'Credible in mature-node lithography, and not a factor at the leading edge — which is what "competitive" means here.', ), p( 'Canon', @@ -657,7 +657,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Lithography and nanoimprint', 'competitive', - 'An alternative path that has not yet displaced anything at volume.' + 'An alternative path that has not yet displaced anything at volume.', ), // ---------------- Fabrication ---------------- @@ -667,7 +667,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['TW'], 'Leading-edge foundry', 'chokepoint', - 'A handful of fabs can run the newest node at volume, and one of them runs most of it.' + 'A handful of fabs can run the newest node at volume, and one of them runs most of it.', ), p( 'Samsung Foundry', @@ -675,7 +675,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR'], 'Leading-edge foundry and memory', 'concentrated', - 'The only other merchant foundry credibly at the leading edge.' + 'The only other merchant foundry credibly at the leading edge.', ), p( 'Intel Foundry', @@ -683,7 +683,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US', 'IE', 'IL'], 'Leading-edge foundry', 'concentrated', - 'The main non-Asian leading-edge option, and the reason several policy programmes exist.' + 'The main non-Asian leading-edge option, and the reason several policy programmes exist.', ), p( 'SMIC', @@ -691,7 +691,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CN'], 'Foundry', 'concentrated', - 'Domestic Chinese capacity operating under equipment restrictions — the constraint is imported, not technical.' + 'Domestic Chinese capacity operating under equipment restrictions — the constraint is imported, not technical.', ), p( 'GlobalFoundries', @@ -699,7 +699,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US', 'DE', 'SG'], 'Mature and specialty nodes', 'competitive', - 'Mature-node capacity is genuinely contested, which is exactly why it is not where the chain binds.' + 'Mature-node capacity is genuinely contested, which is exactly why it is not where the chain binds.', ), p( 'UMC', @@ -707,7 +707,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['TW'], 'Mature-node foundry', 'competitive', - 'Same: plenty of credible suppliers at these nodes.' + 'Same: plenty of credible suppliers at these nodes.', ), // ---------------- Packaging & memory ---------------- @@ -717,7 +717,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['TW'], 'CoWoS-class packaging', 'chokepoint', - 'Accelerator output is gated by packaging slots, not wafer starts, and they are allocated years ahead.' + 'Accelerator output is gated by packaging slots, not wafer starts, and they are allocated years ahead.', ), p( 'ASE Technology', @@ -725,7 +725,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['TW'], 'Assembly and test', 'concentrated', - 'The largest OSAT, moving up into advanced packaging as demand overflows.' + 'The largest OSAT, moving up into advanced packaging as demand overflows.', ), p( 'Amkor', @@ -733,7 +733,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US', 'KR'], 'Assembly and test', 'concentrated', - 'The main non-Taiwanese OSAT of scale, and a policy favourite for that reason.' + 'The main non-Taiwanese OSAT of scale, and a policy favourite for that reason.', ), p( 'SK hynix', @@ -741,7 +741,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR'], 'High-bandwidth memory', 'chokepoint', - 'HBM stacking yield is knowledge that does not transfer when a competitor buys the same equipment.' + 'HBM stacking yield is knowledge that does not transfer when a competitor buys the same equipment.', ), p( 'Micron', @@ -749,7 +749,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US', 'JP', 'SG'], 'High-bandwidth memory', 'concentrated', - 'One of three, and the only one headquartered outside Korea.' + 'One of three, and the only one headquartered outside Korea.', ), p( 'Samsung Memory', @@ -757,7 +757,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['KR'], 'High-bandwidth memory', 'concentrated', - 'Enormous capacity, with qualification at the top of the HBM range a separate question from volume.' + 'Enormous capacity, with qualification at the top of the HBM range a separate question from volume.', ), // ---------------- Systems & silicon ---------------- @@ -767,7 +767,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Accelerators and interconnect', 'chokepoint', - 'The constraint is not only silicon: the software estate around it is what makes substitution slow even where alternatives exist.' + 'The constraint is not only silicon: the software estate around it is what makes substitution slow even where alternatives exist.', ), p( 'AMD', @@ -775,7 +775,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Accelerators and CPUs', 'concentrated', - 'The credible merchant alternative, gated by the same packaging and memory as everyone else.' + 'The credible merchant alternative, gated by the same packaging and memory as everyone else.', ), p( 'Broadcom', @@ -783,7 +783,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Custom accelerators and networking silicon', 'concentrated', - 'Most large in-house accelerator programmes run through a very short list of design partners.' + 'Most large in-house accelerator programmes run through a very short list of design partners.', ), p( 'Marvell', @@ -791,7 +791,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Custom silicon, optics and interconnect', 'concentrated', - 'The other name on that short list.' + 'The other name on that short list.', ), p( 'Vertiv', @@ -799,7 +799,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Datacentre power and thermal systems', 'concentrated', - 'Rack-level power and cooling became a constraint the moment density outran air.' + 'Rack-level power and cooling became a constraint the moment density outran air.', ), p( 'Arista Networks', @@ -807,7 +807,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Datacentre networking', 'competitive', - 'Several credible suppliers of high-speed switching, and merchant silicon underneath most of them.' + 'Several credible suppliers of high-speed switching, and merchant silicon underneath most of them.', ), p( 'Supermicro', @@ -815,7 +815,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US', 'TW'], 'Server systems integration', 'competitive', - 'Integration capacity is contested; the parts going into it are not.' + 'Integration capacity is contested; the parts going into it are not.', ), // ---------------- Energy & grid ---------------- @@ -825,7 +825,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CH', 'JP'], 'Transformers, HVDC, grid equipment', 'chokepoint', - 'Large power transformers run to multi-year lead times, and a datacentre cannot be energised without one.' + 'Large power transformers run to multi-year lead times, and a datacentre cannot be energised without one.', ), p( 'Siemens Energy', @@ -833,7 +833,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE'], 'Grid equipment and turbines', 'chokepoint', - 'Order books for both halves of the energisation problem are effectively spoken for.' + 'Order books for both halves of the energisation problem are effectively spoken for.', ), p( 'GE Vernova', @@ -841,7 +841,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Gas turbines and grid equipment', 'chokepoint', - 'The fastest route to firm power at scale, sold out well into the future.' + 'The fastest route to firm power at scale, sold out well into the future.', ), p( 'Prysmian', @@ -849,7 +849,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['IT'], 'High-voltage cable', 'concentrated', - 'The unglamorous half of energisation, with the same inability to answer a demand shock quickly.' + 'The unglamorous half of energisation, with the same inability to answer a demand shock quickly.', ), p( 'NKT', @@ -857,7 +857,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DK'], 'High-voltage cable', 'concentrated', - 'A short list of firms able to make and lay HV cable at all.' + 'A short list of firms able to make and lay HV cable at all.', ), p( 'Schneider Electric', @@ -865,7 +865,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['FR'], 'Electrical distribution and datacentre power', 'concentrated', - 'Switchgear and distribution have deepened into a constraint alongside transformers.' + 'Switchgear and distribution have deepened into a constraint alongside transformers.', ), p( 'ABB', @@ -873,7 +873,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CH'], 'Electrification and drives', 'concentrated', - 'Present on both the power and the motion side of this chain.' + 'Present on both the power and the motion side of this chain.', ), p( 'Mitsubishi Electric', @@ -881,7 +881,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Transformers and power electronics', 'concentrated', - 'One of the few transformer makers with capacity outside Europe and the US.' + 'One of the few transformer makers with capacity outside Europe and the US.', ), // ---------------- Actuation & robotics ---------------- @@ -891,7 +891,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Strain-wave reduction gears', 'chokepoint', - 'Precision drives set what a robot joint can do, and the tolerances are decades of accumulated practice.' + 'Precision drives set what a robot joint can do, and the tolerances are decades of accumulated practice.', ), p( 'Nabtesco', @@ -899,7 +899,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Cycloidal reduction gears', 'chokepoint', - 'The other half of a duopoly that quietly gates humanoid and industrial robotics alike.' + 'The other half of a duopoly that quietly gates humanoid and industrial robotics alike.', ), p( 'FANUC', @@ -907,7 +907,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Industrial robots and CNC', 'concentrated', - 'Vertically integrated down to its own drives and controls, which is itself the moat.' + 'Vertically integrated down to its own drives and controls, which is itself the moat.', ), p( 'Yaskawa', @@ -915,7 +915,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['JP'], 'Servo motors and robots', 'concentrated', - 'Servo and drive expertise that new entrants consistently underestimate.' + 'Servo and drive expertise that new entrants consistently underestimate.', ), p( 'ABB Robotics', @@ -923,7 +923,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['CH', 'SE'], 'Industrial robots', 'concentrated', - 'One of a small number of full-line robot makers worldwide.' + 'One of a small number of full-line robot makers worldwide.', ), p( 'Renishaw', @@ -931,7 +931,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['GB'], 'Encoders and metrology', 'concentrated', - 'Closing the control loop precisely is a narrow specialism.' + 'Closing the control loop precisely is a narrow specialism.', ), p( 'KUKA', @@ -939,7 +939,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['DE', 'CN'], 'Industrial robots', 'competitive', - 'Robot assembly is contested; the drives inside are where the scarcity sits.' + 'Robot assembly is contested; the drives inside are where the scarcity sits.', ), // ---------------- Deployment & demand ---------------- @@ -949,7 +949,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Hyperscale compute buyer', 'concentrated', - 'On the demand side the grade reads the other way: a handful of buyers account for most of the world’s accelerator orders.' + 'On the demand side the grade reads the other way: a handful of buyers account for most of the world’s accelerator orders.', ), p( 'Amazon Web Services', @@ -957,7 +957,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Hyperscale compute buyer and custom silicon', 'concentrated', - 'Buys at a scale that moves supply, and designs around it where it can.' + 'Buys at a scale that moves supply, and designs around it where it can.', ), p( 'Google', @@ -965,7 +965,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Hyperscale compute buyer and custom silicon', 'concentrated', - 'The longest-running in-house accelerator programme, and still bound by the same packaging.' + 'The longest-running in-house accelerator programme, and still bound by the same packaging.', ), p( 'Meta', @@ -973,7 +973,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Hyperscale compute buyer', 'concentrated', - 'Among the largest single sources of demand for everything upstream of it.' + 'Among the largest single sources of demand for everything upstream of it.', ), p( 'OpenAI', @@ -981,7 +981,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Frontier model developer', 'concentrated', - 'Demand large enough to be a planning input for several layers above it in this list.' + 'Demand large enough to be a planning input for several layers above it in this list.', ), p( 'Anthropic', @@ -989,7 +989,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Frontier model developer', 'concentrated', - 'Same: frontier training demand is concentrated in very few organisations.' + 'Same: frontier training demand is concentrated in very few organisations.', ), p( 'xAI', @@ -997,7 +997,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Frontier model developer', 'concentrated', - 'Notable for building its own power and datacentre capacity to get around the queues.' + 'Notable for building its own power and datacentre capacity to get around the queues.', ), p( 'CoreWeave', @@ -1005,7 +1005,7 @@ export const PARTICIPANTS: readonly Participant[] = [ ['US'], 'Specialist compute provider', 'competitive', - 'Neocloud capacity is contested and growing — the constraint is what they buy, not what they sell.' + 'Neocloud capacity is contested and growing — the constraint is what they buy, not what they sell.', ), ]; @@ -1023,23 +1023,23 @@ export interface ParticipantProgress { } export function participantProgress(): ParticipantProgress { - const grade = (g: ScarcityGrade) => PARTICIPANTS.filter(item => item.scarcity === g).length; + const grade = (g: ScarcityGrade) => PARTICIPANTS.filter((item) => item.scarcity === g).length; return { total: PARTICIPANTS.length, - sourced: PARTICIPANTS.filter(item => item.source !== null).length, + sourced: PARTICIPANTS.filter((item) => item.source !== null).length, chokepoints: grade('chokepoint'), concentrated: grade('concentrated'), competitive: grade('competitive'), - jurisdictions: new Set(PARTICIPANTS.flatMap(item => item.jurisdictions)).size, + jurisdictions: new Set(PARTICIPANTS.flatMap((item) => item.jurisdictions)).size, }; } /** Participants in one layer, in the order they were written. */ export function participantsInLayer(layer: ChainLayer): Participant[] { - return PARTICIPANTS.filter(item => item.layer === layer); + return PARTICIPANTS.filter((item) => item.layer === layer); } /** Every participant graded a hard constraint — the point of the exercise. */ export function bindingParticipants(): Participant[] { - return PARTICIPANTS.filter(item => item.scarcity === 'chokepoint'); + return PARTICIPANTS.filter((item) => item.scarcity === 'chokepoint'); } diff --git a/config/substrata.ts b/config/substrata.ts index c346336..8e9d81f 100644 --- a/config/substrata.ts +++ b/config/substrata.ts @@ -41,8 +41,6 @@ export const COMPANY = { tagline: 'The chokepoints between here and the singularity, written down in public.', } as const; - - // ===================================================================== // OWNERSHIP // ===================================================================== @@ -528,7 +526,7 @@ export // ===================================================================== const AREA_BY_ID: Record = COVERAGE_AREAS.reduce( (acc, area) => ({ ...acc, [area.id]: area }), - {} as Record + {} as Record, ); /** @returns the coverage area a material belongs to. */ diff --git a/test/site-content.test.ts b/test/site-content.test.ts index 17b8163..7614c0a 100644 --- a/test/site-content.test.ts +++ b/test/site-content.test.ts @@ -27,7 +27,7 @@ test('the whole site validates against the sitekit schema', () => { test('every nav item resolves to a page that exists', () => { const pages = sitePages(); - const paths = new Set(pages.map(p => p.path)); + const paths = new Set(pages.map((p) => p.path)); for (const item of siteNavItems(pages)) { assert.ok(paths.has(item.path), `nav points at missing page '${item.path}'`); } diff --git a/tsconfig.json b/tsconfig.json index 1a0ca4a..171e04e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": false, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From e3e4806e5f38679753e6f801886b6cb3987ecdf0 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:18:03 +0200 Subject: [PATCH 3/3] chore: teach git blame to skip the reformat Co-Authored-By: Claude Opus 5 --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..d714f92 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs` +59f014a8ac427bd3bfc62aa8fad2bc225b9390c4 # prettier, 9 files