From e7e7a76f87dab015eb8a097296590ed1e635f52e Mon Sep 17 00:00:00 2001 From: Valentina-Alto <57071541+Valentina-Alto@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:36:24 +0400 Subject: [PATCH 1/7] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 219b666..5e93121 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +workshop url: https://github-samples.github.io/copilot-workshops/vscode/0-prerequisites/ + # Tailspin Toys Tailspin Toys is a crowdfunding platform for games with a developer theme. The project is a website for a fictional game crowd-funding company, built as a single [Astro](https://astro.build/) site (fully prerendered/static output) styled with [Tailwind CSS](https://tailwindcss.com/). Its data lives in a local SQLite database accessed through [Drizzle ORM](https://orm.drizzle.team/) and Node.js's built-in SQLite driver; pages query the database directly in frontmatter at build time, so there is no separate backend service. From bb22ce9654698bd22974c14b78f6125d68414bdc Mon Sep 17 00:00:00 2001 From: Valentina-Alto <57071541+Valentina-Alto@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:37:21 +0400 Subject: [PATCH 2/7] Update README with workshop links and images Added links to workshops and images in README. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e93121..2c8aa39 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -workshop url: https://github-samples.github.io/copilot-workshops/vscode/0-prerequisites/ +Basic workshop url: https://github-samples.github.io/copilot-workshops/vscode/0-prerequisites/ +Agentic devops workshop: https://copilot-dev-days.github.io/agentic-workflows-workshop/step.html?step=readme + +Free hack: +image +image + + # Tailspin Toys From 087f86ae29ef4394e98e434ae1a312a38d48f755 Mon Sep 17 00:00:00 2001 From: Valentina-Alto <57071541+Valentina-Alto@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:54:40 +0400 Subject: [PATCH 3/7] Add useful documentation links to DOCUMENT.S.md --- DOCUMENT.S.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 DOCUMENT.S.md diff --git a/DOCUMENT.S.md b/DOCUMENT.S.md new file mode 100644 index 0000000..02ef7b7 --- /dev/null +++ b/DOCUMENT.S.md @@ -0,0 +1,7 @@ +# Useful Docs + +- Tokens optimization: https://olivomarco.github.io/github-copilot-token-optimization/#comparisons +- Agentic DevOps Workshop: https://copilot-dev-days.github.io/agentic-workflows-workshop/step.html?step=readme +- Spec Kit: https://github.com/github/spec-kit +- HVE Core: https://github.com/microsoft/hve-core +- Awesome Copilot: https://awesome-copilot.github.com/ From bf4e50efefb2535b1b32640d2249013b16100327 Mon Sep 17 00:00:00 2001 From: Kamar Shad Date: Wed, 26 Aug 2026 12:37:13 +0000 Subject: [PATCH 4/7] Add category and publisher filtering to the data-access layer Introduce getAllCategories/getAllPublishers helpers and extend getAllGames with optional category/publisher filters (AND across groups, OR within a group), with unit test coverage. --- src/lib/categories.test.ts | 26 ++++++++++ src/lib/categories.ts | 17 +++++++ src/lib/games.test.ts | 98 ++++++++++++++++++++++++++++++++++++++ src/lib/games.ts | 23 +++++++-- src/lib/publishers.test.ts | 26 ++++++++++ src/lib/publishers.ts | 17 +++++++ 6 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 src/lib/categories.test.ts create mode 100644 src/lib/categories.ts create mode 100644 src/lib/publishers.test.ts create mode 100644 src/lib/publishers.ts diff --git a/src/lib/categories.test.ts b/src/lib/categories.test.ts new file mode 100644 index 0000000..22f21de --- /dev/null +++ b/src/lib/categories.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDatabase } from '../../db/test-helpers'; +import { categories } from '../../db/schema'; +import type { Database } from './db'; +import { getAllCategories } from './categories'; + +describe('categories data-access helpers', () => { + let db: Database; + + beforeEach(async () => { + db = await createTestDatabase(); + }); + + it('returns all categories ordered by name', async () => { + await db.insert(categories).values({ name: 'Strategy', description: 'cat' }); + await db.insert(categories).values({ name: 'Arcade', description: 'cat' }); + + const all = await getAllCategories(db); + expect(all.map((c) => c.name)).toEqual(['Arcade', 'Strategy']); + expect(all[0]).toEqual({ id: expect.any(Number), name: 'Arcade' }); + }); + + it('returns an empty array when there are no categories', async () => { + expect(await getAllCategories(db)).toEqual([]); + }); +}); diff --git a/src/lib/categories.ts b/src/lib/categories.ts new file mode 100644 index 0000000..6f89a40 --- /dev/null +++ b/src/lib/categories.ts @@ -0,0 +1,17 @@ +import { asc } from 'drizzle-orm'; +import type { Category } from '../types/game'; +import { categories } from '../../db/schema'; +import type { Database } from './db'; + +/** + * Returns all categories with their IDs and names, ordered by name. + * + * @param db - The database connection to query. + * @returns The categories' IDs and names. + */ +export async function getAllCategories(db: Database): Promise { + return db + .select({ id: categories.id, name: categories.name }) + .from(categories) + .orderBy(asc(categories.name)); +} diff --git a/src/lib/games.test.ts b/src/lib/games.test.ts index efea6b6..9458bb8 100644 --- a/src/lib/games.test.ts +++ b/src/lib/games.test.ts @@ -64,3 +64,101 @@ describe('games data-access helpers', () => { expect(await getGameById(db, 99999)).toBeNull(); }); }); + +async function seedFilterableGames(db: Database): Promise<{ + strategyId: number; + puzzleId: number; + pubOneId: number; + pubTwoId: number; +}> { + const [strategy] = await db + .insert(categories) + .values({ name: 'Strategy', description: 'cat' }) + .returning({ id: categories.id }); + const [puzzle] = await db + .insert(categories) + .values({ name: 'Puzzle', description: 'cat' }) + .returning({ id: categories.id }); + const [pubOne] = await db + .insert(publishers) + .values({ name: 'Pub One', description: 'pub' }) + .returning({ id: publishers.id }); + const [pubTwo] = await db + .insert(publishers) + .values({ name: 'Pub Two', description: 'pub' }) + .returning({ id: publishers.id }); + + await db.insert(games).values({ + title: 'Strategy One', + description: 'd', + starRating: 4, + categoryId: strategy.id, + publisherId: pubOne.id, + }); + await db.insert(games).values({ + title: 'Strategy Two', + description: 'd', + starRating: 4, + categoryId: strategy.id, + publisherId: pubTwo.id, + }); + await db.insert(games).values({ + title: 'Puzzle One', + description: 'd', + starRating: 4, + categoryId: puzzle.id, + publisherId: pubOne.id, + }); + await db.insert(games).values({ + title: 'Puzzle Two', + description: 'd', + starRating: 4, + categoryId: puzzle.id, + publisherId: pubTwo.id, + }); + + return { strategyId: strategy.id, puzzleId: puzzle.id, pubOneId: pubOne.id, pubTwoId: pubTwo.id }; +} + +describe('getAllGames filtering', () => { + let db: Database; + + beforeEach(async () => { + db = await createTestDatabase(); + }); + + it('returns all games when no filters are provided', async () => { + await seedFilterableGames(db); + expect(await getAllGames(db)).toHaveLength(4); + }); + + it('filters games by a single category', async () => { + const { strategyId } = await seedFilterableGames(db); + const filtered = await getAllGames(db, { categoryIds: [strategyId] }); + expect(filtered.map((g) => g.title)).toEqual(['Strategy One', 'Strategy Two']); + }); + + it('filters games by multiple categories (OR within the group)', async () => { + const { strategyId, puzzleId } = await seedFilterableGames(db); + const filtered = await getAllGames(db, { categoryIds: [strategyId, puzzleId] }); + expect(filtered).toHaveLength(4); + }); + + it('filters games by publisher', async () => { + const { pubOneId } = await seedFilterableGames(db); + const filtered = await getAllGames(db, { publisherIds: [pubOneId] }); + expect(filtered.map((g) => g.title)).toEqual(['Puzzle One', 'Strategy One']); + }); + + it('combines category and publisher filters (AND across groups)', async () => { + const { strategyId, pubTwoId } = await seedFilterableGames(db); + const filtered = await getAllGames(db, { categoryIds: [strategyId], publisherIds: [pubTwoId] }); + expect(filtered.map((g) => g.title)).toEqual(['Strategy Two']); + }); + + it('returns an empty array when no games match the combined filters', async () => { + const { puzzleId, pubOneId } = await seedFilterableGames(db); + const filtered = await getAllGames(db, { categoryIds: [puzzleId], publisherIds: [pubOneId + 999] }); + expect(filtered).toEqual([]); + }); +}); diff --git a/src/lib/games.ts b/src/lib/games.ts index 4ed218e..5362cc5 100644 --- a/src/lib/games.ts +++ b/src/lib/games.ts @@ -1,8 +1,21 @@ -import { eq, asc } from 'drizzle-orm'; +import { eq, asc, and, inArray, type SQL } from 'drizzle-orm'; import type { Database } from './db'; import { games, categories, publishers } from '../../db/schema'; import type { Game } from '../types/game'; +/** Optional filters for narrowing the game list by category and/or publisher. */ +export interface GameFilters { + categoryIds?: number[]; + publisherIds?: number[]; +} + +function buildGameFilterCondition(filters?: GameFilters): SQL | undefined { + return and( + filters?.categoryIds?.length ? inArray(games.categoryId, filters.categoryIds) : undefined, + filters?.publisherIds?.length ? inArray(games.publisherId, filters.publisherIds) : undefined, + ); +} + const gameSelection = { id: games.id, title: games.title, @@ -50,9 +63,11 @@ function baseGamesQuery(db: Database) { .leftJoin(publishers, eq(games.publisherId, publishers.id)); } -/** All games ordered by title. */ -export async function getAllGames(db: Database): Promise { - const rows = await baseGamesQuery(db).orderBy(asc(games.title)); +/** All games ordered by title, optionally narrowed by category and/or publisher. */ +export async function getAllGames(db: Database, filters?: GameFilters): Promise { + const rows = await baseGamesQuery(db) + .where(buildGameFilterCondition(filters)) + .orderBy(asc(games.title)); return rows.map(mapGame); } diff --git a/src/lib/publishers.test.ts b/src/lib/publishers.test.ts new file mode 100644 index 0000000..28b6439 --- /dev/null +++ b/src/lib/publishers.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDatabase } from '../../db/test-helpers'; +import { publishers } from '../../db/schema'; +import type { Database } from './db'; +import { getAllPublishers } from './publishers'; + +describe('publishers data-access helpers', () => { + let db: Database; + + beforeEach(async () => { + db = await createTestDatabase(); + }); + + it('returns all publishers ordered by name', async () => { + await db.insert(publishers).values({ name: 'Zed Games', description: 'pub' }); + await db.insert(publishers).values({ name: 'Acme Games', description: 'pub' }); + + const all = await getAllPublishers(db); + expect(all.map((p) => p.name)).toEqual(['Acme Games', 'Zed Games']); + expect(all[0]).toEqual({ id: expect.any(Number), name: 'Acme Games' }); + }); + + it('returns an empty array when there are no publishers', async () => { + expect(await getAllPublishers(db)).toEqual([]); + }); +}); diff --git a/src/lib/publishers.ts b/src/lib/publishers.ts new file mode 100644 index 0000000..c70d65f --- /dev/null +++ b/src/lib/publishers.ts @@ -0,0 +1,17 @@ +import { asc } from 'drizzle-orm'; +import type { Publisher } from '../types/game'; +import { publishers } from '../../db/schema'; +import type { Database } from './db'; + +/** + * Returns all publishers with their IDs and names, ordered by name. + * + * @param db - The database connection to query. + * @returns The publishers' IDs and names. + */ +export async function getAllPublishers(db: Database): Promise { + return db + .select({ id: publishers.id, name: publishers.name }) + .from(publishers) + .orderBy(asc(publishers.name)); +} From d474c3a0471e28ad7a2fff6baac4c399c2c09e12 Mon Sep 17 00:00:00 2001 From: Kamar Shad Date: Wed, 26 Aug 2026 12:37:49 +0000 Subject: [PATCH 5/7] Add category and publisher filter UI to the game list New GameFilters.astro component renders accessible checkbox groups and filters the prerendered game grid client-side; GameCard now exposes category/publisher ids for filtering. --- src/components/GameCard.astro | 2 + src/components/GameFilters.astro | 124 +++++++++++++++++++++++++++++++ src/pages/index.astro | 23 +++++- 3 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/components/GameFilters.astro diff --git a/src/components/GameCard.astro b/src/components/GameCard.astro index da71b54..77d8532 100644 --- a/src/components/GameCard.astro +++ b/src/components/GameCard.astro @@ -16,6 +16,8 @@ const { game } = Astro.props; data-testid="game-card" data-game-id={game.id} data-game-title={game.title} + data-category-id={game.category?.id} + data-publisher-id={game.publisher?.id} >
diff --git a/src/components/GameFilters.astro b/src/components/GameFilters.astro new file mode 100644 index 0000000..f5de26d --- /dev/null +++ b/src/components/GameFilters.astro @@ -0,0 +1,124 @@ +--- +import type { Category, Publisher } from '../types/game'; + +interface Props { + categories: Category[]; + publishers: Publisher[]; +} + +const { categories, publishers } = Astro.props; +--- + +
+
+

Filter games

+ +
+ +
+
+ Category +
+ {categories.map((category) => ( + + ))} +
+
+ +
+ Publisher +
+ {publishers.map((publisher) => ( + + ))} +
+
+
+ +

+
+ + diff --git a/src/pages/index.astro b/src/pages/index.astro index 5ad3cfa..60da4e2 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,12 +3,18 @@ import Layout from '../layouts/Layout.astro'; import GameCard from '../components/GameCard.astro'; import PageHero from '../components/PageHero.astro'; import EmptyState from '../components/EmptyState.astro'; +import GameFilters from '../components/GameFilters.astro'; import { getDatabase } from '../lib/db'; import { getAllGames } from '../lib/games'; +import { getAllCategories } from '../lib/categories'; +import { getAllPublishers } from '../lib/publishers'; export const prerender = true; -const games = await getAllGames(getDatabase()); +const db = getDatabase(); +const games = await getAllGames(db); +const categories = await getAllCategories(db); +const publishers = await getAllPublishers(db); --- @@ -24,10 +30,19 @@ const games = await getAllGames(getDatabase()); {games.length === 0 ? ( ) : ( -
- {games.map((game) => )} -
+ <> + {(categories.length > 0 || publishers.length > 0) && ( + + )} +
+ {games.map((game) => )} +
+ + )}
+ From 8574b0e611f712cdf8872ad3230fc9e61d1fd2c2 Mon Sep 17 00:00:00 2001 From: Kamar Shad Date: Wed, 26 Aug 2026 12:37:53 +0000 Subject: [PATCH 6/7] Add e2e tests for game filtering Covers filter visibility, single/multi-category OR, publisher filter, combined AND, clear filters, and keyboard interaction. --- e2e-tests/filters.spec.ts | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 e2e-tests/filters.spec.ts diff --git a/e2e-tests/filters.spec.ts b/e2e-tests/filters.spec.ts new file mode 100644 index 0000000..69195c2 --- /dev/null +++ b/e2e-tests/filters.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Game Filtering', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await expect(page.getByTestId('games-grid')).toBeVisible(); + }); + + test('should display category and publisher filter controls', async ({ page }) => { + await test.step('Verify the filter panel and groups are visible', async () => { + await expect(page.getByTestId('game-filters')).toBeVisible(); + await expect(page.getByRole('group', { name: 'Category' })).toBeVisible(); + await expect(page.getByRole('group', { name: 'Publisher' })).toBeVisible(); + }); + + await test.step('Verify filter options are unchecked by default', async () => { + const strategyCheckbox = page.getByRole('checkbox', { name: 'Strategy' }); + await expect(strategyCheckbox).not.toBeChecked(); + }); + }); + + test('should filter games by a single category', async ({ page }) => { + const totalCount = await page.getByTestId('game-card').count(); + + await test.step('Select the Strategy category filter', async () => { + await page.getByRole('checkbox', { name: 'Strategy' }).check(); + }); + + await test.step('Verify only Strategy games remain visible', async () => { + const visibleCards = page.locator('[data-testid="game-card"]:visible'); + const categories = await visibleCards.locator('[data-testid="game-category"]').allTextContents(); + expect(categories.length).toBeGreaterThan(0); + expect(categories.every((category) => category === 'Strategy')).toBeTruthy(); + }); + + await test.step('Verify the results count is announced', async () => { + const visibleCount = await page.locator('[data-testid="game-card"]:visible').count(); + await expect(page.getByTestId('filter-results-count')).toHaveText(`Showing ${visibleCount} of ${totalCount} games`); + }); + }); + + test('should combine multiple categories with OR semantics', async ({ page }) => { + await test.step('Select two category filters', async () => { + await page.getByRole('checkbox', { name: 'Strategy' }).check(); + await page.getByRole('checkbox', { name: 'Puzzle' }).check(); + }); + + await test.step('Verify only games from either category remain visible', async () => { + const visibleCards = page.locator('[data-testid="game-card"]:visible'); + const categories = await visibleCards.locator('[data-testid="game-category"]').allTextContents(); + expect(categories.length).toBeGreaterThan(0); + expect(categories.every((category) => category === 'Strategy' || category === 'Puzzle')).toBeTruthy(); + }); + }); + + test('should filter games by publisher', async ({ page }) => { + await test.step('Select a publisher filter', async () => { + const publisherCheckbox = page.getByTestId('publisher-filter').getByRole('checkbox').first(); + const publisherName = await publisherCheckbox.getAttribute('aria-label'); + await publisherCheckbox.check(); + + await test.step('Verify only games from that publisher remain visible', async () => { + const visibleCards = page.locator('[data-testid="game-card"]:visible'); + const publisherNames = await visibleCards.locator('[data-testid="game-publisher"]').allTextContents(); + expect(publisherNames.length).toBeGreaterThan(0); + expect(publisherNames.every((name) => name === publisherName)).toBeTruthy(); + }); + }); + }); + + test('should combine category and publisher filters with AND semantics', async ({ page }) => { + await test.step('Select a category and a publisher filter', async () => { + await page.getByRole('checkbox', { name: 'Strategy' }).check(); + await page.getByRole('checkbox', { name: 'GitHub Games' }).check(); + }); + + await test.step('Verify only games matching both filters remain visible', async () => { + const visibleCards = page.locator('[data-testid="game-card"]:visible'); + await expect(visibleCards).toHaveCount(1); + await expect(visibleCards.getByTestId('game-category')).toHaveText('Strategy'); + await expect(visibleCards.getByTestId('game-publisher')).toHaveText('GitHub Games'); + }); + }); + + test('should clear all filters and restore the full game list', async ({ page }) => { + const totalCount = await page.getByTestId('game-card').count(); + + await test.step('Apply filters', async () => { + await page.getByRole('checkbox', { name: 'Strategy' }).check(); + await page.getByRole('checkbox', { name: 'GitHub Games' }).check(); + await expect(page.locator('[data-testid="game-card"]:visible')).toHaveCount(1); + }); + + await test.step('Clear filters and verify all games are visible again', async () => { + await page.getByTestId('clear-filters-button').click(); + await expect(page.locator('[data-testid="game-card"]:visible')).toHaveCount(totalCount); + await expect(page.getByRole('checkbox', { name: 'Strategy' })).not.toBeChecked(); + await expect(page.getByRole('checkbox', { name: 'GitHub Games' })).not.toBeChecked(); + }); + }); + + test('should support keyboard interaction with filter checkboxes', async ({ page }) => { + const strategyCheckbox = page.getByRole('checkbox', { name: 'Strategy' }); + + await test.step('Focus and toggle the checkbox with the keyboard', async () => { + await strategyCheckbox.focus(); + await expect(strategyCheckbox).toBeFocused(); + await page.keyboard.press('Space'); + await expect(strategyCheckbox).toBeChecked(); + }); + + await test.step('Verify the grid updates in response', async () => { + const categories = await page.locator('[data-testid="game-card"]:visible [data-testid="game-category"]').allTextContents(); + expect(categories.every((category) => category === 'Strategy')).toBeTruthy(); + }); + }); +}); From 211af84071264105738b6514e696918ab9fcd16a Mon Sep 17 00:00:00 2001 From: Kamar Shad Date: Wed, 26 Aug 2026 12:38:00 +0000 Subject: [PATCH 7/7] Document category and publisher filtering feature Update README features section and copilot-instructions repository structure to reflect the new filtering helpers, component, and tests. --- .github/copilot-instructions.md | 8 ++++---- README.md | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4fc2812..8132532 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -89,12 +89,12 @@ This is a crowdfunding platform for games with a developer theme. The applicatio The application lives at the repository root: - `db/`: Drizzle schema, migrations, transforms, seed, and `games.csv` -- `src/lib/`: Node SQLite client (`db.ts`) and data-access helpers (`games.ts`) -- `src/components/`: reusable `.astro` components +- `src/lib/`: Node SQLite client (`db.ts`) and data-access helpers (`games.ts` with category/publisher filtering, `categories.ts`, `publishers.ts`) +- `src/components/`: reusable `.astro` components (including `GameFilters.astro` for category/publisher filtering) - `src/layouts/`: Astro layout templates -- `src/pages/`: Astro page routes (`index.astro` listing, `game/[id].astro`, `404.astro`, `about.astro`) +- `src/pages/`: Astro page routes (`index.astro` listing with filters, `game/[id].astro`, `404.astro`, `about.astro`) - `src/styles/`: CSS and Tailwind configuration - `src/types/`: TypeScript interfaces (Game, Publisher, Category) -- `e2e-tests/`: Playwright E2E tests (home, games, accessibility) +- `e2e-tests/`: Playwright E2E tests (home, games, accessibility, filters) - `drizzle.config.ts`, `vitest.config.ts`, `astro.config.mjs`, `playwright.config.ts`: tooling config - `README.md`: Project documentation diff --git a/README.md b/README.md index 2c8aa39..9e1c39d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ Tailspin Toys is a crowdfunding platform for games with a developer theme. The p The database is migrated and seeded automatically before `dev`/`build` (via the `predev`/`prebuild` npm scripts) and is written to the gitignored `tailspin.db` file. +## Features + +- **Game catalog** (`/`) — lists all games as cards, each linking to a details page. +- **Category & publisher filtering** — the homepage includes filter controls (`src/components/GameFilters.astro`) so visitors can narrow the catalog by one or more categories and/or a publisher; filters combine (category AND publisher, with OR between multiple selections in the same group). Since the site is fully prerendered, filtering runs client-side over the already-rendered game grid — no rebuild or server round-trip is needed. The underlying data-access helpers (`getAllGames` in `src/lib/games.ts`, plus `getAllCategories`/`getAllPublishers` in `src/lib/categories.ts`/`src/lib/publishers.ts`) also support filtering directly for reuse and testing. + ## Using this template This repository is a GitHub template. When you create a new repository from it, a one-time **Bootstrap template issues** workflow (`.github/workflows/bootstrap-issues.yml`) runs automatically on the first push to `main` and opens a set of starter issues describing suggested first features. Each issue is defined by a Markdown file in `.github/bootstrap-issues/` — the first heading becomes the issue title and the remaining content becomes the body — so you can edit, add, or remove files there to control which issues are created.