From bf515937062e438ffa30fb33606eee179a372023 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Thu, 28 May 2026 17:41:10 -0700 Subject: [PATCH] fix(worktree): make generated workspace names unique across repos (#2857) Auto-generated workspace names walked the marine-creature list in order and deduped only within the active repo (the default when workspaces are nested), so the first auto-named worktree in every repo landed on "Nautilus". That guaranteed identical branch names across repos, which appear flat and indistinguishable in the sidebar. The in-order walk also produced "Nautilus-2", "Nautilus-3", ... within a single repo: the suggester ignores branches left behind by deleted worktrees, so it kept re-proposing "Nautilus", and the create-time retry loop suffixed it to dodge the lingering branch. Random selection now yields a fresh creature each time instead of marching the same name. - Dedup against worktrees in every repo, not just the active one - Pick randomly from the unused pool instead of the first list entry - Lowercase the result to match branch-name convention (fix/seahorse) - Expand the corpus 260 -> 552 (more marine species plus public-domain mythological sea creatures) so random picks rarely repeat getSuggestedCreatureName drops the now-unused repoId/nestWorkspaces params; the RNG is injectable for deterministic tests. Co-authored-by: Jinwoo-H Co-authored-by: Orca --- .../scripts/marine-creatures-parity.test.mjs | 16 + .../components/worktree-name-suggestion.ts | 25 +- mobile/src/constants/marine-creatures.ts | 297 +++++++++++++++++- .../sidebar/worktree-name-suggestions.test.ts | 96 +++--- .../sidebar/worktree-name-suggestions.ts | 53 ++-- src/renderer/src/hooks/useComposerState.ts | 9 +- src/shared/marine-creatures.ts | 297 +++++++++++++++++- 7 files changed, 702 insertions(+), 91 deletions(-) create mode 100644 config/scripts/marine-creatures-parity.test.mjs diff --git a/config/scripts/marine-creatures-parity.test.mjs b/config/scripts/marine-creatures-parity.test.mjs new file mode 100644 index 000000000..33b51a5d3 --- /dev/null +++ b/config/scripts/marine-creatures-parity.test.mjs @@ -0,0 +1,16 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +function readCreatureNames(path) { + const source = readFileSync(path, 'utf8') + return Array.from(source.matchAll(/^ '([^']+)',?$/gm), (match) => match[1]) +} + +describe('marine creature corpus mirrors', () => { + it('keeps the mobile mirror in parity with the shared corpus', () => { + const sharedNames = readCreatureNames('src/shared/marine-creatures.ts') + const mobileNames = readCreatureNames('mobile/src/constants/marine-creatures.ts') + + expect(mobileNames).toEqual(sharedNames) + }) +}) diff --git a/mobile/src/components/worktree-name-suggestion.ts b/mobile/src/components/worktree-name-suggestion.ts index 59ec500f6..55665b8c5 100644 --- a/mobile/src/components/worktree-name-suggestion.ts +++ b/mobile/src/components/worktree-name-suggestion.ts @@ -23,20 +23,29 @@ function normalize(name: string): string { return name.trim().toLowerCase() } -export function getSuggestedCreatureName(existingPaths: readonly string[]): string { +function pickRandom(items: readonly T[], random: () => number): T { + return items[Math.floor(random() * items.length)] +} + +// Why: pick randomly from the unused pool (not the first in list order) so +// fresh worktrees don't all default to "Nautilus" and collide across repos. +export function getSuggestedCreatureName( + existingPaths: readonly string[], + random: () => number = Math.random +): string { const used = new Set() for (const p of existingPaths) { used.add(normalize(pathBasename(p))) } - for (const candidate of MARINE_CREATURES) { - if (!used.has(normalize(candidate))) return candidate - } + // Lowercased to match branch-name convention (fix/seahorse, not fix/Seahorse). + const available = MARINE_CREATURES.map(normalize).filter((name) => !used.has(name)) + if (available.length > 0) return pickRandom(available, random) let suffix = 2 while (true) { - for (const candidate of MARINE_CREATURES) { - const numbered = `${candidate}-${suffix}` - if (!used.has(normalize(numbered))) return numbered - } + const numbered = MARINE_CREATURES.map((name) => `${normalize(name)}-${suffix}`).filter( + (name) => !used.has(name) + ) + if (numbered.length > 0) return pickRandom(numbered, random) suffix += 1 } } diff --git a/mobile/src/constants/marine-creatures.ts b/mobile/src/constants/marine-creatures.ts index c3f1df7f0..fd245bd74 100644 --- a/mobile/src/constants/marine-creatures.ts +++ b/mobile/src/constants/marine-creatures.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines -- Curated marine-creature name corpus; one entry per line is the readable format for a flat data list. */ export const MARINE_CREATURES = [ 'Nautilus', 'Seahorse', @@ -258,5 +259,299 @@ export const MARINE_CREATURES = [ 'Leviathan', 'Wolffish', 'Wreckfish', - 'Horseshoe' + 'Horseshoe', + 'Orca', + 'Cachalot', + 'Rorqual', + 'Grampus', + 'Humpback', + 'Bowhead', + 'Finback', + 'Sealion', + 'Pinniped', + 'Cetacean', + 'Mako', + 'Porbeagle', + 'Dogfish', + 'Spurdog', + 'Tope', + 'Bonnethead', + 'Sixgill', + 'Sevengill', + 'Angelshark', + 'Houndshark', + 'Megamouth', + 'Cookiecutter', + 'Bramble', + 'Ghostshark', + 'Sandtiger', + 'Galeocerdo', + 'Nursehound', + 'Cownose', + 'Devilray', + 'Eagleray', + 'Butterflyray', + 'Numbfish', + 'Stingaree', + 'Mantaray', + 'Tuna', + 'Albacore', + 'Bonito', + 'Skipjack', + 'Wahoo', + 'Kingfish', + 'Mackerel', + 'Cero', + 'Dorado', + 'Mahimahi', + 'Escolar', + 'Pomfret', + 'Butterfish', + 'Cod', + 'Haddock', + 'Pollock', + 'Whiting', + 'Hake', + 'Ling', + 'Cusk', + 'Burbot', + 'Saithe', + 'Sablefish', + 'Lingcod', + 'Greenling', + 'Rockling', + 'Halibut', + 'Flounder', + 'Sole', + 'Plaice', + 'Turbot', + 'Brill', + 'Dab', + 'Fluke', + 'Megrim', + 'Sanddab', + 'Herring', + 'Sardine', + 'Anchovy', + 'Sprat', + 'Pilchard', + 'Menhaden', + 'Shad', + 'Alewife', + 'Salmon', + 'Trout', + 'Char', + 'Grayling', + 'Steelhead', + 'Kokanee', + 'Chinook', + 'Coho', + 'Sockeye', + 'Taimen', + 'Huchen', + 'Cisco', + 'Inconnu', + 'Vendace', + 'Whitefish', + 'Perch', + 'Bass', + 'Snapper', + 'Grouper', + 'Seabass', + 'Tilefish', + 'Bream', + 'Porgy', + 'Sheepshead', + 'Pinfish', + 'Scup', + 'Tautog', + 'Sander', + 'Zander', + 'Walleye', + 'Sauger', + 'Ruffe', + 'Comber', + 'Hind', + 'Coney', + 'Graysby', + 'Margate', + 'Pompano', + 'Amberjack', + 'Yellowtail', + 'Scad', + 'Runner', + 'Mullet', + 'Goatfish', + 'Threadfin', + 'Snook', + 'Barramundi', + 'Milkfish', + 'Mojarra', + 'Weakfish', + 'Corbina', + 'Queenfish', + 'Kahawai', + 'Tilapia', + 'Cunner', + 'Tuskfish', + 'Razorfish', + 'Tang', + 'Porcupinefish', + 'Burrfish', + 'Eel', + 'Wolfeel', + 'Gardeneel', + 'Ribboneel', + 'Cuskeel', + 'Catfish', + 'Cory', + 'Ratfish', + 'Elephantfish', + 'Goosefish', + 'Monkfish', + 'Sargassum', + 'Coffinfish', + 'Seadevil', + 'Scorpionfish', + 'Rockfish', + 'Cabezon', + 'Rosefish', + 'Redfish', + 'Bocaccio', + 'Thornyhead', + 'Gurnard', + 'Searobin', + 'Fangtooth', + 'Bristlemouth', + 'Barreleye', + 'Spookfish', + 'Telescopefish', + 'Lancetfish', + 'Tripodfish', + 'Spiderfish', + 'Daggertooth', + 'Pearleye', + 'Ridgehead', + 'Snaggletooth', + 'Sturgeon', + 'Sterlet', + 'Kaluga', + 'Cockle', + 'Mussel', + 'Clam', + 'Oyster', + 'Scallop', + 'Abalone', + 'Periwinkle', + 'Cerith', + 'Turban', + 'Tellin', + 'Geoduck', + 'Quahog', + 'Piddock', + 'Shipworm', + 'Lobster', + 'Crayfish', + 'Shrimp', + 'Prawn', + 'Langostino', + 'Langouste', + 'Yabby', + 'Marron', + 'Dungeness', + 'Seafan', + 'Staghorn', + 'Elkhorn', + 'Hydroid', + 'Zoanthid', + 'Gorgonian', + 'Seanettle', + 'Moonjelly', + 'Sanddollar', + 'Brittlestar', + 'Basketstar', + 'Sunstar', + 'Seacucumber', + 'Seabiscuit', + 'Hearturchin', + 'Bristleworm', + 'Featherduster', + 'Lugworm', + 'Sandworm', + 'Palolo', + 'Arrowworm', + 'Acornworm', + 'Flatworm', + 'Ribbonworm', + 'Booby', + 'Frigatebird', + 'Tropicbird', + 'Noddy', + 'Kittiwake', + 'Dovekie', + 'Shag', + 'Anhinga', + 'Skimmer', + 'Jaeger', + 'Oystercatcher', + 'Eider', + 'Scoter', + 'Merganser', + 'Brant', + 'Ridley', + 'Flatback', + 'Terrapin', + 'Seasnake', + 'Diatom', + 'Plankton', + 'Larvacean', + 'Doliolid', + 'Kelp', + 'Seagrass', + 'Eelgrass', + 'Dulse', + 'Bladderwrack', + 'Mola', + // Mythological sea & water creatures from public-domain folklore — joining + // Kraken, Leviathan, Siren, Triton, Hydra, Medusa, Nereid, and Selkie above. + 'Scylla', + 'Charybdis', + 'Cetus', + 'Proteus', + 'Glaucus', + 'Hippocamp', + 'Jormungandr', + 'Hafgufa', + 'Kelpie', + 'Merrow', + 'Nuckelavee', + 'Afanc', + 'Rusalka', + 'Vodyanoy', + 'Umibozu', + 'Isonade', + 'Ningyo', + 'Mizuchi', + 'Naga', + 'Makara', + 'Bunyip', + 'Taniwha', + 'Marakihau', + 'Lusca', + 'Undine', + 'Nixie', + 'Melusine', + 'Ondine', + 'Tiamat', + 'Dagon', + 'Aspidochelone', + 'Capricorn', + 'Merfolk', + 'Merman', + 'Mermaid', + 'Timingila', + 'Bakekujira', + 'Jiaolong', + 'Encantado', + 'Hraesvelg' ] as const diff --git a/src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts b/src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts index 04cded5f0..75c267730 100644 --- a/src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts +++ b/src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts @@ -7,97 +7,79 @@ import { shouldApplySuggestedName } from './worktree-name-suggestions' +// Always selects the first element of the unused pool, so assertions are exact. +const pickFirst = () => 0 +// Suggestions are lowercased (branch-name convention), so expectations are too. +const lower = (index: number) => MARINE_CREATURES[index].toLowerCase() + describe('getSuggestedCreatureName', () => { - it('returns the first creature name when no repo is selected', () => { - expect(getSuggestedCreatureName('', {}, false)).toBe(MARINE_CREATURES[0]) + it('picks the first unused name when the RNG selects index 0', () => { + expect(getSuggestedCreatureName({}, pickFirst)).toBe(lower(0)) }) - it('skips names already used in the selected repo', () => { + it('dedupes against worktrees in EVERY repo, not just one', () => { expect( getSuggestedCreatureName( - 'repo-1', { - 'repo-1': [{ path: '/tmp/worktrees/Nautilus' }, { path: '/tmp/worktrees/Seahorse' }] + 'repo-1': [{ path: '/tmp/worktrees/Nautilus' }], + 'repo-2': [{ path: '/tmp/worktrees/Seahorse' }] }, - true + pickFirst ) - ).toBe('Starfish') + ).toBe(lower(2)) }) - it('checks all repos when nestWorkspaces is false', () => { + it('never reuses a name already used in another repo', () => { + // Regression guard: the old per-repo scoping would have returned Nautilus + // here because the active repo had no worktrees of its own. expect( getSuggestedCreatureName( - 'repo-1', { 'repo-1': [], - 'repo-2': [{ path: '/tmp/worktrees/Nautilus' }] + 'repo-2': [{ path: `/tmp/worktrees/${MARINE_CREATURES[0]}` }] }, - false + pickFirst ) - ).toBe('Seahorse') + ).toBe(lower(1)) }) - it('only checks the selected repo when nestWorkspaces is true', () => { - expect( - getSuggestedCreatureName( - 'repo-1', - { - 'repo-1': [], - 'repo-2': [{ path: '/tmp/worktrees/Nautilus' }] - }, - true - ) - ).toBe('Nautilus') + it('selects randomly from the unused pool', () => { + // random() = i/N ⇒ pickRandom returns the pool's i-th entry. + const pickIndex = (index: number, poolSize: number) => () => index / poolSize + expect(getSuggestedCreatureName({}, pickIndex(2, MARINE_CREATURES.length))).toBe(lower(2)) + expect(getSuggestedCreatureName({}, pickIndex(5, MARINE_CREATURES.length))).toBe(lower(5)) }) it('falls back to suffixed variants after the base list is exhausted', () => { const usedWorktrees = MARINE_CREATURES.map((name) => ({ path: `/tmp/worktrees/${name}` })) - expect( - getSuggestedCreatureName( - 'repo-1', - { - 'repo-1': usedWorktrees - }, - true - ) - ).toBe(`${MARINE_CREATURES[0]}-2`) + expect(getSuggestedCreatureName({ 'repo-1': usedWorktrees }, pickFirst)).toBe(`${lower(0)}-2`) }) it('treats used names case-insensitively', () => { expect( - getSuggestedCreatureName( - 'repo-1', - { - 'repo-1': [{ path: '/tmp/worktrees/nAuTiLuS' }] - }, - true - ) - ).toBe('Seahorse') + getSuggestedCreatureName({ 'repo-1': [{ path: '/tmp/worktrees/nAuTiLuS' }] }, pickFirst) + ).toBe(lower(1)) }) it('handles Windows-style worktree paths when deriving used basenames', () => { expect( - getSuggestedCreatureName( - 'repo-1', - { - 'repo-1': [{ path: 'C:\\worktrees\\Nautilus' }] - }, - true - ) - ).toBe('Seahorse') + getSuggestedCreatureName({ 'repo-1': [{ path: 'C:\\worktrees\\Nautilus' }] }, pickFirst) + ).toBe(lower(1)) }) it('handles stored worktree paths with trailing separators', () => { expect( getSuggestedCreatureName( - 'repo-1', { - 'repo-1': [{ path: 'C:\\worktrees\\Nautilus\\\\' }, { path: '/tmp/worktrees/Seahorse///' }] + 'repo-1': [ + { path: 'C:\\worktrees\\Nautilus\\\\' }, + { path: '/tmp/worktrees/Seahorse///' } + ] }, - true + pickFirst ) - ).toBe('Starfish') + ).toBe(lower(2)) }) }) @@ -118,7 +100,7 @@ describe('shouldApplySuggestedName', () => { describe('MARINE_CREATURES', () => { it('is non-empty and unique after normalization and sanitization', () => { - expect(MARINE_CREATURES.length).toBeGreaterThanOrEqual(260) + expect(MARINE_CREATURES.length).toBeGreaterThanOrEqual(500) const normalizedNames = MARINE_CREATURES.map(normalizeSuggestedName) const sanitizedNames = MARINE_CREATURES.map((name) => sanitizeWorktreeName(name)) @@ -136,7 +118,13 @@ describe('MARINE_CREATURES', () => { 'Hogchoker', 'Hogsucker', 'Mudsucker', - 'Hardhead' + 'Hardhead', + // Real marine organisms, but the bare word reads as a fruit, flower, or + // land insect rather than something from the sea. + 'Olive', + 'Tulip', + 'Cone', + 'Mantis' ] for (const disallowedName of disallowedNames) { diff --git a/src/renderer/src/components/sidebar/worktree-name-suggestions.ts b/src/renderer/src/components/sidebar/worktree-name-suggestions.ts index 63ebeebe7..2356a7e56 100644 --- a/src/renderer/src/components/sidebar/worktree-name-suggestions.ts +++ b/src/renderer/src/components/sidebar/worktree-name-suggestions.ts @@ -5,39 +5,46 @@ type WorktreePathLike = { path: string } -export function getSuggestedCreatureName( - repoId: string, - worktreesByRepo: Record, - nestWorkspaces: boolean -): string { - if (!repoId) { - return MARINE_CREATURES[0] - } - +// Why: dedup across every repo, not just the active one — branch names appear +// flat in the sidebar, so per-repo scoping let two repos collide on one name. +function collectUsedNames(worktreesByRepo: Record): Set { const usedNames = new Set() - const relevantWorktrees = nestWorkspaces - ? [worktreesByRepo[repoId] ?? []] - : Object.values(worktreesByRepo) - - for (const worktrees of relevantWorktrees) { + for (const worktrees of Object.values(worktreesByRepo)) { for (const worktree of worktrees) { usedNames.add(normalizeSuggestedName(basename(worktree.path))) } } + return usedNames +} - for (const candidate of MARINE_CREATURES) { - if (!usedNames.has(normalizeSuggestedName(candidate))) { - return candidate - } +function pickRandom(items: readonly T[], random: () => number): T { + return items[Math.floor(random() * items.length)] +} + +export function getSuggestedCreatureName( + worktreesByRepo: Record, + random: () => number = Math.random +): string { + const usedNames = collectUsedNames(worktreesByRepo) + + // Why: names are lowercased (branch names are conventionally lowercase, e.g. + // fix/seahorse), and a random pick keeps fresh worktrees from all starting at + // the same creature and marching down the list in lockstep. + const available = MARINE_CREATURES.map(normalizeSuggestedName).filter( + (name) => !usedNames.has(name) + ) + if (available.length > 0) { + return pickRandom(available, random) } + // Every base name is taken — fall back to numbered variants. let suffix = 2 while (true) { - for (const candidate of MARINE_CREATURES) { - const numberedCandidate = `${candidate}-${suffix}` - if (!usedNames.has(normalizeSuggestedName(numberedCandidate))) { - return numberedCandidate - } + const numbered = MARINE_CREATURES.map( + (name) => `${normalizeSuggestedName(name)}-${suffix}` + ).filter((name) => !usedNames.has(name)) + if (numbered.length > 0) { + return pickRandom(numbered, random) } suffix += 1 } diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 500ad1a94..08aa47de7 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -654,12 +654,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const shouldWaitForSetupCheck = Boolean(selectedRepo) && selectedRepoIsGit && isSetupCheckPending // Why: when the user leaves the workspace name blank and provides no other - // seed source (prompt, linked issue/PR), pick a repo-scoped unique marine + // seed source (prompt, linked issue/PR), pick a globally-unique marine // creature name so the workspace gets a distinct, readable identifier - // instead of colliding on a literal "workspace" default. + // instead of colliding on a literal "workspace" default — or on the same + // creature already used in another repo. const fallbackCreatureName = useMemo( - () => getSuggestedCreatureName(repoId, worktreesByRepo, settings?.nestWorkspaces ?? true), - [repoId, worktreesByRepo, settings?.nestWorkspaces] + () => getSuggestedCreatureName(worktreesByRepo), + [worktreesByRepo] ) const workspaceSeedName = useMemo( () => diff --git a/src/shared/marine-creatures.ts b/src/shared/marine-creatures.ts index 78e8c0714..3402be669 100644 --- a/src/shared/marine-creatures.ts +++ b/src/shared/marine-creatures.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines -- Curated marine-creature name corpus; one entry per line is the readable format for a flat data list. */ // Why: the auto-generated workspace name pool lives in shared (not renderer) // so the main process can recognize an Orca-generated branch name when deciding // whether auto-rename-from-work is allowed to overwrite it. @@ -261,5 +262,299 @@ export const MARINE_CREATURES = [ 'Leviathan', 'Wolffish', 'Wreckfish', - 'Horseshoe' + 'Horseshoe', + 'Orca', + 'Cachalot', + 'Rorqual', + 'Grampus', + 'Humpback', + 'Bowhead', + 'Finback', + 'Sealion', + 'Pinniped', + 'Cetacean', + 'Mako', + 'Porbeagle', + 'Dogfish', + 'Spurdog', + 'Tope', + 'Bonnethead', + 'Sixgill', + 'Sevengill', + 'Angelshark', + 'Houndshark', + 'Megamouth', + 'Cookiecutter', + 'Bramble', + 'Ghostshark', + 'Sandtiger', + 'Galeocerdo', + 'Nursehound', + 'Cownose', + 'Devilray', + 'Eagleray', + 'Butterflyray', + 'Numbfish', + 'Stingaree', + 'Mantaray', + 'Tuna', + 'Albacore', + 'Bonito', + 'Skipjack', + 'Wahoo', + 'Kingfish', + 'Mackerel', + 'Cero', + 'Dorado', + 'Mahimahi', + 'Escolar', + 'Pomfret', + 'Butterfish', + 'Cod', + 'Haddock', + 'Pollock', + 'Whiting', + 'Hake', + 'Ling', + 'Cusk', + 'Burbot', + 'Saithe', + 'Sablefish', + 'Lingcod', + 'Greenling', + 'Rockling', + 'Halibut', + 'Flounder', + 'Sole', + 'Plaice', + 'Turbot', + 'Brill', + 'Dab', + 'Fluke', + 'Megrim', + 'Sanddab', + 'Herring', + 'Sardine', + 'Anchovy', + 'Sprat', + 'Pilchard', + 'Menhaden', + 'Shad', + 'Alewife', + 'Salmon', + 'Trout', + 'Char', + 'Grayling', + 'Steelhead', + 'Kokanee', + 'Chinook', + 'Coho', + 'Sockeye', + 'Taimen', + 'Huchen', + 'Cisco', + 'Inconnu', + 'Vendace', + 'Whitefish', + 'Perch', + 'Bass', + 'Snapper', + 'Grouper', + 'Seabass', + 'Tilefish', + 'Bream', + 'Porgy', + 'Sheepshead', + 'Pinfish', + 'Scup', + 'Tautog', + 'Sander', + 'Zander', + 'Walleye', + 'Sauger', + 'Ruffe', + 'Comber', + 'Hind', + 'Coney', + 'Graysby', + 'Margate', + 'Pompano', + 'Amberjack', + 'Yellowtail', + 'Scad', + 'Runner', + 'Mullet', + 'Goatfish', + 'Threadfin', + 'Snook', + 'Barramundi', + 'Milkfish', + 'Mojarra', + 'Weakfish', + 'Corbina', + 'Queenfish', + 'Kahawai', + 'Tilapia', + 'Cunner', + 'Tuskfish', + 'Razorfish', + 'Tang', + 'Porcupinefish', + 'Burrfish', + 'Eel', + 'Wolfeel', + 'Gardeneel', + 'Ribboneel', + 'Cuskeel', + 'Catfish', + 'Cory', + 'Ratfish', + 'Elephantfish', + 'Goosefish', + 'Monkfish', + 'Sargassum', + 'Coffinfish', + 'Seadevil', + 'Scorpionfish', + 'Rockfish', + 'Cabezon', + 'Rosefish', + 'Redfish', + 'Bocaccio', + 'Thornyhead', + 'Gurnard', + 'Searobin', + 'Fangtooth', + 'Bristlemouth', + 'Barreleye', + 'Spookfish', + 'Telescopefish', + 'Lancetfish', + 'Tripodfish', + 'Spiderfish', + 'Daggertooth', + 'Pearleye', + 'Ridgehead', + 'Snaggletooth', + 'Sturgeon', + 'Sterlet', + 'Kaluga', + 'Cockle', + 'Mussel', + 'Clam', + 'Oyster', + 'Scallop', + 'Abalone', + 'Periwinkle', + 'Cerith', + 'Turban', + 'Tellin', + 'Geoduck', + 'Quahog', + 'Piddock', + 'Shipworm', + 'Lobster', + 'Crayfish', + 'Shrimp', + 'Prawn', + 'Langostino', + 'Langouste', + 'Yabby', + 'Marron', + 'Dungeness', + 'Seafan', + 'Staghorn', + 'Elkhorn', + 'Hydroid', + 'Zoanthid', + 'Gorgonian', + 'Seanettle', + 'Moonjelly', + 'Sanddollar', + 'Brittlestar', + 'Basketstar', + 'Sunstar', + 'Seacucumber', + 'Seabiscuit', + 'Hearturchin', + 'Bristleworm', + 'Featherduster', + 'Lugworm', + 'Sandworm', + 'Palolo', + 'Arrowworm', + 'Acornworm', + 'Flatworm', + 'Ribbonworm', + 'Booby', + 'Frigatebird', + 'Tropicbird', + 'Noddy', + 'Kittiwake', + 'Dovekie', + 'Shag', + 'Anhinga', + 'Skimmer', + 'Jaeger', + 'Oystercatcher', + 'Eider', + 'Scoter', + 'Merganser', + 'Brant', + 'Ridley', + 'Flatback', + 'Terrapin', + 'Seasnake', + 'Diatom', + 'Plankton', + 'Larvacean', + 'Doliolid', + 'Kelp', + 'Seagrass', + 'Eelgrass', + 'Dulse', + 'Bladderwrack', + 'Mola', + // Mythological sea & water creatures from public-domain folklore — joining + // Kraken, Leviathan, Siren, Triton, Hydra, Medusa, Nereid, and Selkie above. + 'Scylla', + 'Charybdis', + 'Cetus', + 'Proteus', + 'Glaucus', + 'Hippocamp', + 'Jormungandr', + 'Hafgufa', + 'Kelpie', + 'Merrow', + 'Nuckelavee', + 'Afanc', + 'Rusalka', + 'Vodyanoy', + 'Umibozu', + 'Isonade', + 'Ningyo', + 'Mizuchi', + 'Naga', + 'Makara', + 'Bunyip', + 'Taniwha', + 'Marakihau', + 'Lusca', + 'Undine', + 'Nixie', + 'Melusine', + 'Ondine', + 'Tiamat', + 'Dagon', + 'Aspidochelone', + 'Capricorn', + 'Merfolk', + 'Merman', + 'Mermaid', + 'Timingila', + 'Bakekujira', + 'Jiaolong', + 'Encantado', + 'Hraesvelg' ] as const