AnchorStack

good-first-issue Security & Engineering Audit

You shipped it.
Is it safe to operate?

We audited good-first-issue across 9 engineering areas - 46 findings, 10 of them critical or high. Here's the picture.

https://github.com/DeepSourceCorp/good-first-issue

fail - Prototype / high risk4.5 of 10
0critical
10high
19medium
17low
Area Scores

9 areas evaluated

Each area scored 0-10 by an AnchorStack engineer.

Code Quality

5.1/10

The codebase is small, clean, and easy to orient to, but it carries meaningful quality debt concentrated in two areas: the Python data pipeline (populate.py) has zero test coverage of its business logic despite being the sole production data source, and the entire Vue/JS frontend is written in plain JavaScript with no TypeScript, leaving type errors invisible and a real null-dereference bug reachable by any visitor. CI enforces Python sanity tests and mypy but skips frontend type-checking and build verification entirely. For a low-risk public static site the blast radius is limited, but changes to the pipeline or frontend carry no safety net.

watchWeight x1

Architecture

7.2/10

Good First Issue is a well-matched architecture for its purpose: a Nuxt 3 SSG static site on Vercel, fed by a daily Python pipeline that scrapes GitHub and stages data through Vercel Blob storage. The synchronous/asynchronous boundaries are clean — there is no runtime server, no mutable state under user load, and no database to fail. The main architectural risks are downstream of the pipeline: the generated JSON contract between populate.py and the Vue pages is entirely implicit, the blob overwrite has no versioning or quality gate, and pipeline failures are silent. For a content-curation site at current scale these are manageable gaps, but the lack of a data quality gate before deploy is the one scenario where a bad cron run can push a degraded site with no recovery path.

watchWeight x1

Data

6/10

Good First Issue has no relational database; its data layer is a Python scrape pipeline that writes two JSON files (generated.json, tags.json) to Vercel Blob for Nuxt SSG to consume at build time. This architecture is well-matched to the project, and sensitive data risk is minimal — no PII is collected or stored. The primary risks are silent corruption and schema drift: the pipeline overwrites blob storage with no atomicity guarantee between the two files, there is no minimum-output threshold before upload, the sample data file has diverged from the actual pipeline output schema (stale objectID field, wrong datetime format), and the language slug page has a null-dereference path if tags.json and generated.json become inconsistent. Data validation covers only the input TOML; the generated output is entirely unvalidated before upload or consumption.

watchWeight x1.5

Security

4.7/10

Good First Issue is a fully public static site with no user accounts, server-side logic, or sensitive data at runtime, which eliminates the largest classes of web application security risk. The dominant concern is supply-chain: the daily CI cron job uses a third-party GitHub Action pinned to a floating @master ref that executes with access to all three production secrets. No dependency vulnerability scanning exists, and security response headers are absent from the Vercel deployment. The SubscriptionForm also submits user emails to an undocumented third-party ListMonk instance with no privacy disclosure. Overall risk posture is low-to-medium for a read-only content site, but the floating CI action ref warrants prompt remediation.

failWeight x1.5

AI Engineering

2.7/10

Good First Issue has minimal AI engineering maturity: the only AI-related infrastructure is a brief disclosure-and-review policy in CONTRIBUTING.md, which is proportionate for an open-source project that does not rely on AI for its own delivery pipeline. No agent context files (CLAUDE.md, AGENTS.md, cursor rules) exist, meaning AI tools assisting contributors have no knowledge of the project's architecture, the implicit Python-to-Vue data contract, the absence of TypeScript in the frontend, or the critical pipeline invariants. For a small public static site with no sensitive workflows, the absolute risk is low — but any AI-assisted contribution to the data pipeline or Vue components would be working entirely blind, making it easy to introduce or compound the structural gaps already identified in other audit domains. The fix is lightweight: a single CLAUDE.md or AGENTS.md capturing the key constraints a contributor's AI tool needs would close most of this gap.

failWeight x1

Infrastructure

4.5/10

Good First Issue runs on a fully managed Vercel SSG deployment fed by a daily GitHub Actions cron pipeline — a well-matched hosting shape for a public, read-heavy, content-curation site with no runtime server. The platform fit is excellent and scaling risk for serving is negligible. The main infrastructure concerns are operational: the Vercel project configuration is dashboard-only and cannot be rebuilt from the repository alone; the deploy pipeline (cron webhook) is completely decoupled from the CI test gate, so a bad commit can be deployed without test validation; there is no blob versioning and no documented recovery path; and all secret-bearing workflows execute on a third-party Blacksmith runner rather than GitHub-hosted infrastructure. For an open-source project at this maturity these are manageable gaps, but the absence of a CI-to-deploy gate and the lack of a documented rebuild procedure are the highest-priority items.

failWeight x1

Observability

2.3/10

Good First Issue has near-zero production observability. The only telemetry is Google Analytics (loaded via nuxt-gtag with no measurement ID visible in the repository, making its effectiveness unverifiable) and loguru stdout logs in the daily Python pipeline that flow only to GitHub Actions log history. There is no error tracking, no uptime monitoring, no alerting, no heartbeat check on the daily cron job, and no runbooks. The most dangerous blind spot is that the daily pipeline — the site's only data source — fails completely silently: a crashed populate.py, a failed blob upload, or an empty-output run triggers no notification and may not be noticed until users report stale data. For a public open-source project at this maturity, minimum viable observability (cron heartbeat, one uptime check, a free Sentry DSN on the frontend) is achievable in a few hours and would close the largest gaps.

failWeight x1

Delivery

4.2/10

Good First Issue uses a two-workflow GitHub Actions setup: ci.yml runs data-sanity tests and mypy on every push, and cron.yml drives a daily pipeline that populates data, syncs to Vercel Blob, and fires a deploy webhook. The tooling choices are appropriate for a static SSG site, but the delivery posture has a critical structural gap: the two workflows are completely decoupled. cron.yml neither runs its own test suite nor checks whether ci.yml passed on the current HEAD before deploying, so any commit that breaks the test suite still reaches production on the next cron cycle. Compounding this, there is no evidence of branch protection rules enforcing required reviews or required CI status checks before merge to main. The result is a pipeline where both merge-gate discipline and the deploy gate are entirely voluntary. For a public content site with no PII the blast radius is bounded, but the missing CI-to-deploy gate, absent branch protection, and no Blob data versioning mean recovery from a bad deploy depends on manual action with no documented runbook. Changes are shipped without any rollout controls, and there is no feature-flag mechanism. These gaps are addressable with low engineering effort given the simple hosting shape.

failWeight x1

Documentation

2.6/10

Good First Issue has a minimal documentation footprint: a community-facing README and a contributor setup guide (CONTRIBUTING.md) are the only substantive docs. These cover local frontend setup well but leave critical gaps — there is no .env.example or discoverable env-var inventory, no architecture or data-flow documentation, no operational runbooks for any scenario, no API or schema docs, and no AI context files. A new maintainer could run the frontend locally but could not understand the full system, configure or recover the data pipeline, or respond to any incident without reverse-engineering the codebase and CI configs. The documentation posture is adequate only for casual open-source contributions; it would fail any handoff or on-call scenario involving the data pipeline or production environment.

failWeight x1
Findings

Where to look first

01

highCOD-001Code Quality

Zero test coverage for data pipeline business logic

populate.py contains all production data-generation logic — GitHub API calls, rate-limit handling, inactivity filtering, data transformation — but the only tests that exist validate TOML file format and deduplication.

Impact

Bugs in API error handling, rate-limiter state, data field mapping, or filtering conditions will silently corrupt or truncate the daily-generated dataset without any automated signal. The site would serve stale or wrong data.

Remediation

Add unit tests for get_repository_info covering at least: (1) archived repo is skipped, (2) inactive repo is skipped, (3) ForbiddenError triggers retry and coordinated pause, (4) NotFoundError returns None, (5) valid repo produces correctly shaped output dict. Mock github3 client responses. Also add a test that the generated JSON output matches the expected schema before it is uploaded to Vercel Blob.

02

highCOD-002Code Quality

Null-dereference on unrecognised language slug in route page

Tags.find() in pages/language/[slug].vue returns undefined when the slug does not match any known language, and tag.language is immediately accessed with no null guard, throwing a TypeError at runtime.

Impact

Any visitor who navigates to /language/<unknown-slug> (via a mistyped URL, a stale bookmark, or a web crawler) will hit a JavaScript TypeError that crashes the page render. In SSG this may also break the build if a slug mismatch occurs during pre-rendering.

Remediation

Guard the find result before use: `const tag = Tags.find(t => t.slug === route.params.slug); if (!tag) { throw createError({ statusCode: 404, statusMessage: 'Language not found' }); }`. Alternatively use Nuxt's `navigateTo` or show a user-facing 404. Apply the same guard pattern to the equivalent lookup in default.vue.

03

highSEC-001Security

CI cron action pinned to floating @master ref — all production secrets exposed to supply-chain compromise

cron.yml uses joelwmale/webhook-action@master, a floating branch reference; any commit pushed to that repo's master branch runs with access to GH_ACCESS_TOKEN, BLOB_READ_WRITE_TOKEN, and DEPLOY_HOOK_URL.

Impact

A compromised or malicious commit to joelwmale/webhook-action@master would execute arbitrary code in the cron job environment, exfiltrating all three production secrets. With GH_ACCESS_TOKEN an attacker could read or modify repository content; with BLOB_READ_WRITE_TOKEN they could overwrite the production blob store and deploy arbitrary site content via the deploy hook.

Remediation

Replace @master with a pinned commit SHA: look up the current HEAD SHA of joelwmale/webhook-action (e.g. uses: joelwmale/webhook-action@<sha>). Alternatively, replace the action entirely with a native curl step (curl -X POST "${{ secrets.DEPLOY_HOOK_URL }}") which eliminates the third-party dependency. Add a Dependabot config for github-actions to receive automated SHA-update PRs going forward.

04

highINF-001Infrastructure

Cron deploy webhook fires independently of CI — broken commits can be deployed without test validation

cron.yml triggers a Vercel deploy hook as its final step regardless of whether ci.yml has passed on the current HEAD; the two workflows share no dependency or status gate.

Impact

A commit that fails the CI test suite (data-sanity checks, mypy type checks) will still reach production on the next scheduled cron run (or manual trigger). If a contributor merges a commit that breaks populate.py type annotations or corrupts the TOML structure, the daily cron will run the broken pipeline and attempt to deploy the result. There is no circuit-breaker between code quality validation and production deployment.

Remediation

Add a GitHub Actions workflow rule that requires ci.yml to succeed on the current default-branch HEAD before cron.yml proceeds to the deploy step. This can be implemented with a preliminary step that calls the GitHub API to check the latest ci.yml run status on the default branch and exits non-zero if it has not passed. Alternatively, restructure cron.yml to include the CI steps (pre-build, test) before running the data pipeline so a single workflow enforces both gates.

05

highOBS-001Observability

Daily cron pipeline fails completely silently — no heartbeat monitor or failure alert

cron.yml has no heartbeat check, missed-run detection, or failure notification; if populate.py crashes, the blob upload errors, or the deploy hook returns a non-200, the only evidence is buried in GitHub Actions log history with no maintainer notification.

Impact

The daily pipeline is the sole mechanism for keeping the site's data current. A silent failure means the site serves stale data indefinitely — potentially for days or weeks — until a user reports it or a maintainer happens to check the Actions tab. A completely empty output (all repos filtered out due to a bug) would overwrite production data with an empty list and still produce no alert.

Remediation

Add a free heartbeat monitor using Healthchecks.io or Cronitor (both have generous free tiers). Add a `curl -fsS --retry 3 <heartbeat-ping-url>` step at the end of cron.yml that only fires when all preceding steps succeed; configure the monitor to alert after one missed ping. Separately, add a minimum-repo-count assertion in populate.py before writing generated.json (e.g., raise RuntimeError if len(REPOSITORIES) < 50) so an empty-output run fails the pipeline rather than silently overwriting production data.

06

highOBS-002Observability

No error tracking on any surface — frontend JS errors and pipeline exceptions go unreported

Neither the Nuxt static frontend nor the Python data pipeline is connected to any error tracking service; uncaught JavaScript exceptions in production and Python stack traces from the pipeline are invisible to maintainers.

Impact

Frontend JavaScript errors (network failures fetching blob data, Vue rendering errors, subscription form failures) are never captured; there is no way to know if users are encountering them at scale. Python pipeline exceptions that are not already caught (e.g., unexpected GitHub API response shapes, malformed TOML after a contributor error) would appear only in GitHub Actions logs and produce no notification. The absence of error tracking means regressions introduced by contributors can go undetected until user complaints accumulate.

Remediation

For the frontend: install @sentry/nuxt (free Sentry tier is ample for this traffic volume), configure it with a DSN in nuxt.config.ts, and enable source-map upload in the Nuxt build step so stack traces are readable. For the pipeline: add a Sentry Python SDK call in a top-level try/except in populate.py's __main__ block to capture and report unhandled exceptions before exit. Tag both with environment='production' and release=<git-sha> (available as GITHUB_SHA in Actions) for deploy correlation.

07

highDEL-001Delivery

cron.yml skips tests and is not gated on ci.yml — broken commits deploy automatically

The cron deploy workflow runs no test suite of its own and has no dependency on ci.yml status, so a commit that fails CI will deploy to production on the next scheduled cron cycle.

Impact

Any contributor-merged change that breaks test_data.py, mypy, or the Python data pipeline will be executed in production on the next 08:00 UTC cron run. If populate.py fails mid-run or produces malformed JSON that passes the absent output-validation gate, the corrupt data is synced to Vercel Blob and the deploy hook fires, potentially serving a broken or empty site until the next manual intervention. There is no circuit-breaker between code quality validation and production deployment.

Remediation

Restructure cron.yml to run make test (data-sanity checks and mypy) as the first substantive step, before make generate. This ensures the same code quality gate that ci.yml enforces on push also gates the deploy pipeline. As an additional guard, add a preliminary step that calls the GitHub API to assert the latest ci.yml run on the default branch HEAD has a conclusion of 'success' before proceeding, catching cases where CI was skipped or a merge bypassed the check. A failing test step should exit non-zero and abort the pipeline before any data is generated or uploaded.

08

highDEL-002Delivery

No branch protection rules — CI and review requirements are not enforced before merge to main

No CODEOWNERS file, no required-status-check configuration, and no required-reviewer rule were found; merge to main relies entirely on contributor convention rather than enforced controls.

Impact

Any contributor or maintainer with repository write access can push directly to main or merge a PR without CI passing and without a review. This means the ci.yml test gate — the only automated quality signal in the repository — is advisory rather than mandatory. A direct push that introduces a broken data pipeline or malformed TOML will reach production on the next cron cycle with no interception. For an open-source project receiving community PRs at significant volume (PR #2570 observed), the absence of enforced gates increases the likelihood of an accidental bad merge reaching production.

Remediation

Enable branch protection on the main/master branch in GitHub repository Settings → Branches: require at least one approving review before merge, require the CI status check (the test job in ci.yml) to pass before merging, and disallow direct pushes. Add a .github/CODEOWNERS file assigning review responsibility to the core maintainer(s) for critical paths (gfi/*.py, data/repositories.toml, .github/workflows/, nuxt.config.ts) so that PRs touching those paths automatically request reviews from the right people.

09

highDOC-001Documentation

No .env.example and no discoverable env-var inventory — data pipeline is unconfigurable from docs alone

None of the four required environment variables (GH_ACCESS_TOKEN, BLOB_READ_WRITE_TOKEN, DEPLOY_HOOK_URL, NUXT_PUBLIC_GTAG_ID) are documented in any user-facing file; they are only discoverable by reading the Makefile, GitHub Actions workflow files, and nuxt.config.ts.

Impact

A new contributor or incident responder reading CONTRIBUTING.md and following its steps will successfully start the frontend with sample data but will be completely blocked the moment they try to run the data pipeline, sync Blob storage, or rebuild production. The BLOB_READ_WRITE_TOKEN required by the Vercel build (make generate-prod) is not mentioned in vercel.json, README, or CONTRIBUTING.md — an operator recreating the Vercel project would not know to set it. A missing or expired GH_ACCESS_TOKEN causes a silent data failure with no documented recovery path.

Remediation

Add a .env.example file at the repository root listing every required and optional environment variable with a description and example value: GH_ACCESS_TOKEN (GitHub personal access token for scraping), BLOB_READ_WRITE_TOKEN (Vercel Blob read/write token for sync and production build), DEPLOY_HOOK_URL (CI-only Vercel deploy webhook URL, optional locally), NUXT_PUBLIC_GTAG_ID (Google Analytics measurement ID, optional). Add a 'Required environment variables' table to CONTRIBUTING.md that explains which variables are needed for each workflow: local dev, data generation, blob sync, and production build. Expand vercel.json with an env block listing build-time variables by name so the Vercel project can be reconstructed from the repository.

10

highDOC-002Documentation

README describes the community project but not the technical system — new maintainers cannot understand what they are maintaining

README.md is written entirely for community contributors submitting new repositories; it contains no stack overview, no architecture description, no deployment shape, and no link to operational resources beyond CONTRIBUTING.md.

Impact

A new maintainer or engineering handoff recipient reading the README learns only that the project curates beginner-friendly issues and accepts new project submissions via a Google Form. They learn nothing about the Nuxt SSG site, the daily Python pipeline, Vercel Blob data storage, GitHub Actions cron, or the Vercel deployment. Any incident investigation, architectural change, or onboarding of a new technical maintainer begins from near-zero context. The absence of a technical overview amplifies the impact of every other documentation gap.

Remediation

Add a 'How it works' or 'Architecture' section to README.md (6-10 lines is sufficient) explaining: the Nuxt 3 SSG frontend deployed on Vercel; the daily GitHub Actions cron job that runs the Python scraper, syncs output to Vercel Blob, and triggers a Vercel deploy webhook; the repositories.toml source-of-truth data file; and the data flow from TOML → generated.json → Vercel Blob → Nuxt build. Add a 'Stack' line listing the primary technologies (Nuxt 3, Bun, Python, Vercel, Vercel Blob, GitHub Actions). This section transforms the README from a community document into a dual-purpose document usable by both contributors and maintainers.

11

mediumCOD-003Code Quality

Vue frontend entirely in plain JavaScript — no TypeScript

All six Vue components and both composable/sync scripts use plain JS script blocks; no <script lang="ts"> is present anywhere in the frontend.

Impact

Type errors, wrong prop shapes, and undefined-property accesses (as in COD-002) are invisible to editors and CI. As the data schema evolves, mismatches between the generated JSON shape and component expectations will not be caught until runtime.

Remediation

Migrate script blocks to <script setup lang="ts"> and define typed interfaces for the repo and issue shapes consumed from generated.json. At minimum, add `bun x nuxt typecheck` (or `vue-tsc --noEmit`) to the CI test job so type regressions are caught on every push.

12

mediumCOD-004Code Quality

Frontend build and type-check absent from CI

ci.yml runs only Python tests and mypy; the Nuxt application is never built or type-checked in CI, so a broken import, a missing data file reference, or a TypeScript error would be invisible until the Vercel deploy runs.

Impact

Frontend regressions — broken imports, component errors, missing generated data — can reach the Vercel build undetected. The CI green status gives false confidence about frontend health.

Remediation

Add a step to ci.yml that runs `bun install && bun x nuxt typecheck` (or `bun generate` using sample data). At minimum add a ruff check step (`uv run ruff check .`) and a prettier check step (`bunx prettier --check .`) so formatting/lint gates are enforced rather than just available.

13

mediumARC-001Architecture

Generated JSON data contract is implicit — no schema between pipeline and frontend

populate.py writes generated.json and tags.json with an undocumented field structure; Vue pages consume these via direct import with no TypeScript types or validation.

Impact

Any populate.py field rename or structural change causes a silent build-time regression or renders missing/undefined data to users. The CI test suite (test_data.py) validates source TOML structure but not the generated output schema, so the breakage only surfaces on build.

Remediation

Define a TypeScript interface (or Zod schema) for the Repository and Tag shapes and import it in both the Vue pages and sync.js. Add a CI step or a post-generate hook that validates generated.json against the schema before syncing to Blob. Alternatively, export a JSON Schema from the Python side using Pydantic and share it across the toolchain.

14

mediumARC-002Architecture

No data quality gate or blob versioning before deploy — bad pipeline run overwrites production data

The cron workflow runs populate.py, immediately syncs its output to Vercel Blob, then triggers a Vercel deploy — with no assertion that the generated data meets a minimum quality threshold.

Impact

If populate.py produces an empty result set (GitHub API outage, expired token, network error returning empty data rather than an exception), the empty or severely degraded generated.json is uploaded and the subsequent build deploys an effectively blank site. There is no prior-version snapshot to roll back to, and no alert to maintainers.

Remediation

Before running `bun sync up`, assert a minimum repo count threshold in populate.py (e.g., abort if fewer than N repositories were collected). Use Vercel Blob versioned upload paths (e.g., timestamped keys) so the previous generation remains retrievable. Consider adding a post-sync smoke check in the cron workflow that validates the uploaded blob before triggering the deploy hook.

15

mediumDAT-001Data

Sample data file has diverged from current pipeline output — stale schema documentation

data/generated.sample.json includes an objectID field and an RFC 2822 last_modified string that do not match the fields or date format written by the current populate.py.

Impact

The sample file is the only concrete documentation of the generated JSON shape. Developers adding frontend features or modifying populate.py will build against an inaccurate schema, increasing the likelihood of introducing field-name bugs or incorrect date-parsing logic that only surfaces at build time or in the browser.

Remediation

Regenerate data/generated.sample.json by running populate.py locally against a small subset of repositories, or update it manually to match current output (remove objectID, update last_modified to ISO 8601 isoformat). Add a CI assertion or post-generate script that diffs the sample against a known-good generated output to catch future drift automatically.

16

mediumDAT-002Data

Non-atomic dual-file blob upload creates a consistency window between generated.json and tags.json

sync.js uploads generated.json then tags.json sequentially with no atomicity guarantee; a failure between the two uploads leaves the site serving the new repository list with the previous day's tag counts and slugs.

Impact

If generated.json is updated but tags.json is not, language pages (/language/[slug]) will filter against the new repository data but route against stale or missing tag slugs. In the worst case, a language that newly appears in generated.json will have no matching entry in tags.json, causing the Sidebar to show stale counts and the language slug page to hit the null-dereference path described in DAT-003.

Remediation

Wrap the upload loop in sync.js with explicit error checking and abort if either file fails. For stronger atomicity, upload both files to a versioned path (e.g., blobs keyed by date or run ID) and only update a pointer file (e.g., latest.json) once both uploads confirm success. This also provides a rollback path absent today.

17

mediumSEC-002Security

No HTTP security response headers configured on deployed site

vercel.json and nuxt.config.ts define no response headers; the live site ships with no Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, or Permissions-Policy.

Impact

Without X-Frame-Options or CSP frame-ancestors, the site can be embedded in an iframe for clickjacking attacks targeting the subscription form. Without X-Content-Type-Options, browsers may MIME-sniff served assets. A CSP would also limit the blast radius if a dependency or external script were compromised. The site's public favicon and static assets are similarly unprotected.

Remediation

Add a headers block to vercel.json: set X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: geolocation=(), microphone=(), camera=(), and a permissive-but-explicit Content-Security-Policy (e.g. default-src 'self'; script-src 'self' 'unsafe-inline' www.googletagmanager.com; form-action https://listmonk.deepsource.io). Verify headers with securityheaders.com post-deploy.

18

mediumSEC-003Security

No dependency vulnerability scanning or secrets detection in CI

Neither workflow (ci.yml, cron.yml) runs SCA, SAST, or secrets scanning; no Dependabot configuration exists for npm, Python, or GitHub Actions dependencies.

Impact

Known CVEs in npm packages (@vercel/blob, dayjs, heroicons) or Python packages (github3.py, requests, python-slugify) would not be surfaced automatically. Accidentally committed secrets (tokens, keys) would not be detected before reaching the remote. As the repository grows and community contributors add dependencies, the attack surface expands silently.

Remediation

Enable GitHub Dependabot by adding .github/dependabot.yml with ecosystem entries for npm, pip, and github-actions on a weekly schedule. Enable GitHub secret scanning (repository Settings → Security) and consider adding a pre-commit hook using gitleaks or detect-secrets to catch secrets before they are committed. Optionally add a pip-audit or safety step in ci.yml for Python CVE scanning.

19

mediumAIE-001AI Engineering

No AI context file — contributors' AI tools have zero project knowledge

The repository contains no CLAUDE.md, AGENTS.md, .cursor/rules, or equivalent context file; AI tools assisting contributors have no awareness of the project's architecture, the implicit Python-to-Vue data contract, the absence of TypeScript in the frontend, or the pipeline's critical invariants.

Impact

AI-assisted contributions to populate.py, the Vue components, or the sync pipeline are generated without knowledge of the constraints that matter most — the undocumented JSON schema, the SSG build assumptions, or the type-safety gaps already present. This makes AI-generated changes the most likely source of new instances of the structural issues found in COD-001, COD-002, COD-003, and ARC-001.

Remediation

Add a CLAUDE.md (or AGENTS.md) at the repository root with: (1) project purpose and architecture overview (SSG static site + daily Python pipeline via Vercel Blob), (2) the generated.json/tags.json field contract and that it has no TypeScript types yet, (3) the fact that all Vue script blocks are plain JS and must not assume TypeScript, (4) the test commands to run before submitting (make test), (5) key things NOT to do (do not modify generated.json directly, do not skip uv run mypy, do not use @master action refs). This file should be fewer than 100 lines and written once by a maintainer.

20

mediumAIE-002AI Engineering

No documented project tenets — AI tools and contributors share no understanding of what 'correct' means

The repository has no written tenets, principles, or design rules beyond a three-bullet AI disclosure policy. Architecture decisions, quality expectations, UI/design consistency, and data-pipeline invariants exist only as implicit maintainer knowledge.

Impact

Without documented tenets, AI-assisted contributions and human contributors alike have no shared reference for what tradeoffs to preserve. The tag-slug duplication across four components (COD-005), the implicit JSON schema (ARC-001), and the missing frontend typecheck (COD-004) are all examples of gaps that documented tenets would prevent by telling contributors what 'done' looks like.

Remediation

Document the project's key tenets in CONTRIBUTING.md or a dedicated TENETS.md: (1) the generated data pipeline is the site's single source of truth — changes to populate.py output must be validated against the frontend schema, (2) new Vue components should not re-implement existing composable logic, (3) CI must remain green before merge, (4) external dependencies (third-party service URLs, CI actions) must be pinned. These need not be long — a short numbered list with one sentence each is sufficient.

21

mediumINF-002Infrastructure

Vercel project configuration is dashboard-only — production environment cannot be rebuilt from the repository

vercel.json contains only buildCommand and framework; Vercel project settings including custom domain (goodfirstissue.dev), required build-time environment variables (BLOB_READ_WRITE_TOKEN), team membership, and deployment protection rules exist only in the Vercel dashboard.

Impact

If the Vercel project is deleted or access is lost, there is no repo-based artifact from which to recreate it. A new contributor or incident responder cannot reconstruct the production deployment from the repository alone. The BLOB_READ_WRITE_TOKEN required by make generate-prod (via bun sync down) is never mentioned in vercel.json, README, or any setup guide — its presence in the Vercel dashboard is invisible to anyone reading the repo.

Remediation

Expand vercel.json to document the full deployment shape: add an env block listing all required build-time environment variables by name (not value), add any rewrites, headers, or output directory configuration currently managed via dashboard, and document the custom domain and deployment protection settings. Add a 'Vercel environment variables required' section to the README or CONTRIBUTING.md alongside the existing GitHub Actions secrets list. This does not require IaC — Vercel's JSON config file is the appropriate artifact.

22

mediumINF-003Infrastructure

No staging environment or documented preview-deployment workflow — all changes go directly to production data pipeline

The repository defines no dev or staging environment; Vercel preview deployments are an implicit platform behavior with no configuration, gating, or documentation in the repo. The cron pipeline writes directly to the production Vercel Blob store.

Impact

Changes to populate.py, sync.js, or nuxt.config.ts have no intermediate environment in which to verify behavior before the next cron-triggered production deploy. A contributor changing the data pipeline cannot validate their change against the live Blob data or the deployed Nuxt build without either running locally or merging to main and waiting for the cron. Bugs introduced by pipeline changes are discovered only after they affect the production site.

Remediation

Document the Vercel preview deployment workflow (currently implicit) in CONTRIBUTING.md: explain that every PR receives a Vercel preview URL, and that contributors should validate data-pipeline changes locally using bun sync down before raising a PR. For higher confidence, consider adding a staging Vercel Blob store (separate BLOB_READ_WRITE_TOKEN) and a staging deploy hook that the cron workflow can target for a dry-run validation before triggering the production hook.

23

mediumOBS-003Observability

No uptime monitoring for goodfirstissue.dev — site downtime discovered only by user complaints

No external synthetic uptime check is configured for the production site; a Vercel incident, a bad deploy, or CDN misconfiguration would go undetected until users report the site as unreachable.

Impact

The project has no way to distinguish between 'site is up' and 'site is down' without a user report. For a public tool used by developers finding their first open-source contribution, extended downtime is a poor experience with no mitigation path. The current daily cron deploy (no post-deploy smoke test) means a broken build could be live for 24 hours before the next cron run, with no alert in between.

Remediation

Add a free external uptime check using UptimeRobot, Better Stack (formerly Logtail Uptime), or Vercel's built-in availability monitoring. Configure it to check https://goodfirstissue.dev every 5 minutes and alert a maintainer email or post to a GitHub issue/Slack channel on downtime. This takes under 10 minutes to set up and requires no code changes.

24

mediumOBS-004Observability

nuxt-gtag loaded with no measurement ID in repository — analytics effectiveness unverifiable

nuxt-gtag is declared in nuxt.config.ts modules with no options block or measurement ID; the Google Analytics property ID must be set via an undocumented Vercel build-time environment variable or analytics is silently inactive.

Impact

If NUXT_PUBLIC_GTAG_ID is not set in the Vercel project dashboard (which is undocumented in the repo), the nuxt-gtag module loads but sends no data to any GA property — the only traffic telemetry tool is silently non-functional. New contributors and operators reading the repo have no way to confirm analytics are working. Even if configured, GA is not an operational observability tool and provides no pipeline health, error rate, or latency signals.

Remediation

Add a gtag block to nuxt.config.ts: `gtag: { id: process.env.NUXT_PUBLIC_GTAG_ID }` and document NUXT_PUBLIC_GTAG_ID in the README alongside other required environment variables. Verify the GA property is receiving hits post-deploy using GA's Realtime report. Note that GA alone is not a substitute for error tracking or uptime monitoring.

25

mediumOBS-005Observability

loguru is a dev-only dependency but required by the production pipeline — fragile install path

pyproject.toml declares loguru under [project.optional-dependencies] dev rather than [project.dependencies]; the production cron pipeline only works because cron.yml runs uv sync --all-extras, silently including all dev extras.

Impact

If cron.yml is updated to use uv sync (without --all-extras) — a natural change when separating dev from prod installs — the pipeline would fail at import time with an ImportError on loguru. This failure would be silent until the next cron run and would eliminate all pipeline logging, making incident diagnosis harder. It also means the production install is larger than intended (all dev tools including mypy, ruff, etc. are installed in the cron environment).

Remediation

Move loguru from [project.optional-dependencies] dev to [project.dependencies] in pyproject.toml, since it is used in production code (populate.py). Regenerate uv.lock after the change. Separately, consider moving mypy, ruff, and types-* stubs to a separate dev extra so the production install is leaner.

26

mediumDEL-003Delivery

CI pipeline missing linting, frontend type-check, and build verification gates

ci.yml runs only Python data-sanity tests and mypy; the Nuxt frontend has no TypeScript type-check, no linting, and no build step in CI — a broken frontend build would not be caught before deploy.

Impact

A change to nuxt.config.ts, a Vue component, or a TypeScript composable that introduces a type error or breaks the static build would not be detected by CI. The failure would surface only when the Vercel deploy hook triggers a build on the Vercel platform — after the cron pipeline has already run and uploaded data. If the build fails at that point, the site stays on the previous Vercel deployment, but there is no alert and no automatic recovery signal in the CI pipeline. Additionally, formatting violations are never caught automatically, creating inconsistent code style over time as community contributions accumulate.

Remediation

Add the following steps to ci.yml: (1) bun install followed by bunx nuxt typecheck (or npx nuxi typecheck) to type-check the Nuxt frontend; (2) bun generate (or make build) to verify the static build succeeds; (3) make format --check (or bunx prettier --check . && uv run ruff check .) to gate on formatting and lint. These steps should run on the same Blacksmith runner already used and will complete in under two minutes for a project of this size. Mark all three as required status checks in branch protection (see DEL-002).

27

mediumDEL-004Delivery

No feature-flag or rollout-control mechanism — all production changes ship atomically with no kill-switch

No feature-flag system of any kind exists; every change to the UI, data pipeline, or site configuration ships to all users simultaneously on the next cron-triggered deploy with no ability to stage, limit, or roll back a specific change independently.

Impact

If a UI change introduces a regression visible only in certain browsers, or a data-pipeline change produces unexpected output, the only remediation paths are reverting the commit and waiting for the next cron cycle, or manually triggering a workflow_dispatch run after reverting — both involve a multi-hour window before correction. For a content-curation site with no PII or payments, the blast radius is limited, but the inability to disable a specific feature or data source without a full revert increases mean time to recovery for any user-visible regression.

Remediation

For the current project maturity, a lightweight approach is sufficient: add a small feature-flags object to nuxt.config.ts public runtime config (runtimeConfig.public.flags) driven by environment variables set in the Vercel dashboard. This allows toggling experimental UI features or new data sources without a code deploy. For data-pipeline changes specifically, the blob versioning strategy recommended in INF-005 provides a complementary rollback lever. A full LaunchDarkly or Unleash integration is not warranted for this project's scale.

28

mediumDOC-003Documentation

No data-flow or architecture documentation — the pipeline's shape and invariants exist only as implicit knowledge

The daily scrape-to-blob-to-deploy pipeline and the implicit JSON contract between populate.py and the Vue pages are not documented anywhere in the repository.

Impact

A contributor changing populate.py output fields has no reference to check against the Vue pages' expectations. A maintainer investigating a site outage cannot reconstruct the data flow (where does generated.json come from? why does the Vercel build need BLOB_READ_WRITE_TOKEN? what is make generate-prod vs make generate?) without reading multiple source files. The absence of data-flow docs amplifies every pipeline-related risk already flagged in architecture and infra domains — there is no recovery guide because there is no diagram to recover from.

Remediation

Add a ARCHITECTURE.md (or a section in CONTRIBUTING.md) covering: (1) the data flow diagram in text form (repositories.toml → populate.py → generated.json/tags.json → Vercel Blob → bun generate → Vercel CDN), (2) the local-dev shortcut (sample JSON bypasses the pipeline), (3) the generated.json schema — at minimum a table of top-level fields consumed by the Vue pages, (4) the Vercel Blob key names used by sync.js. This document is also the natural home for the schema documentation that closes ARC-001.

29

mediumDOC-004Documentation

No operational runbook for any scenario — recovery from pipeline failure, bad deploy, or data corruption requires improvisation

There is no documentation covering how to manually trigger a deploy, recover from a failed cron run, roll back to a previous Vercel deployment, rotate secrets, or handle a corrupted Vercel Blob.

Impact

When the cron pipeline fails silently (as noted in OBS-001), the first maintainer to notice must improvise: find the workflow_dispatch trigger in cron.yml, discover that BLOB_READ_WRITE_TOKEN must be set locally to run sync, realize there is no prior-version blob to restore from, and manually trigger a Vercel redeploy — all without a documented playbook. For an incident that should take 15 minutes to resolve, the absence of a runbook easily extends the resolution time to hours. This also means knowledge lives only in the head of whoever has responded to an incident before.

Remediation

Add a short RUNBOOK.md (or wiki page) covering the five most likely scenarios: (1) cron pipeline failed — how to trigger manually via workflow_dispatch; (2) site is down — how to check Vercel deployment status and trigger a redeploy; (3) data looks stale — how to verify the last successful pipeline run and re-run; (4) secret rotation — which secrets exist, where they are stored (GitHub Actions secrets + Vercel dashboard), and the rotation procedure; (5) adding or removing a repository — the repositories.toml format and the validation steps. This document need not be more than two pages.

30

lowCOD-005Code Quality

Active-tag resolution duplicated across four components

Navbar.vue, default.vue, Sidebar.vue, and pages/language/[slug].vue each independently import Tags and compute or compare route.params.slug to find the active tag.

Impact

If the tag data shape changes or the slug-matching logic needs adjustment, four call sites must be updated consistently. A missed update produces silent staleness (wrong title, wrong active pill, wrong meta).

Remediation

Extract a `useActiveTag()` composable in composables/ that imports Tags once, reads route.params.slug, and returns the resolved tag (or null). All four components replace their local lookup with a single call to this composable.

31

lowCOD-006Code Quality

Sync script uses blocking file I/O inside async functions

syncFilesUp() calls fs.statSync() and fs.readFileSync() inside an async function, blocking the Node.js event loop on disk I/O.

Impact

For the current two-file sync this is harmless in practice, but it is an incorrect pattern that would cause performance problems if the file list grew or if the sync script were reused in a server context.

Remediation

Replace `fs.statSync(filePath)` with `await fs.promises.stat(filePath)` and `fs.readFileSync(filePath)` with `await fs.promises.readFile(filePath)` to keep the function fully async.

32

lowCOD-007Code Quality

RepositoryInfo type alias uses incorrect forward-reference string for key type

populate.py defines `RepositoryInfo = Dict["str", Union[str, int, Sequence]]` — the key type is written as the string literal "str" (a forward reference) rather than the bare type str.

Impact

mypy may silently accept this or produce misleading errors under some configurations. The intent is `Dict[str, ...]`; the forward-reference syntax is only needed when the type itself is not yet defined, which is never the case for builtins.

Remediation

Change the definition to `RepositoryInfo = Dict[str, Union[str, int, Sequence]]`. Consider replacing the loose Dict alias with a proper TypedDict that enumerates each field (name, owner, language, slug, url, stars, etc.) to get precise type checking on field access.

33

lowARC-003Architecture

Daily pipeline has no failure alerting — stale data can persist silently for days

The cron.yml workflow has no on-failure notification step; if the GitHub scrape or blob sync fails, the Vercel deploy hook is not triggered and no maintainer is notified.

Impact

A failed cron run leaves the site serving the previous day's data indefinitely. Without monitoring or alerting, maintainers may not discover the regression until a user reports it, potentially many days later.

Remediation

Add a `if: failure()` step to cron.yml that posts to a Slack webhook, creates a GitHub issue, or sends an email via GitHub Actions' built-in failure notification. GitHub Actions can be configured to send email on workflow failure in repository notification settings as a zero-code option.

34

lowARC-004Architecture

SubscriptionForm hard-codes external service URL — undocumented third-party runtime dependency

SubscriptionForm.vue POSTs to a hard-coded ListMonk instance URL with no error handling; this external service is not documented as a site dependency.

Impact

If the ListMonk endpoint changes domain, goes offline, or rejects requests, the subscription form fails silently or with a CORS error. Users receive no feedback. The dependency is invisible to anyone reading the repo README or deployment docs.

Remediation

Move the ListMonk URL to a build-time environment variable (e.g., NUXT_PUBLIC_LISTMONK_URL) so it appears in configuration and can be overridden. Add basic client-side error handling in the form's submit handler to surface failures to users. Document the ListMonk dependency in README alongside other operational dependencies.

35

lowDAT-003Data

Null dereference on Tags.find() in language slug page crashes SSG pre-render for untagged slugs

pages/language/[slug].vue calls Tags.find() and immediately accesses tag.language without a null guard; populate.py excludes languages with fewer than 3 occurrences from tags.json but can still write those repos into generated.json.

Impact

If a repository's language appears fewer than 3 times in the current run (and therefore is absent from tags.json), but a prior run had included it (and therefore a route was pre-rendered and cached), a new build dereferencing undefined.language will throw during Nuxt's SSG crawl and fail the build or produce a broken page. The risk increases as the corpus evolves and languages gain or lose entries.

Remediation

Add a null guard before accessing tag properties: const tag = Tags.find(t => t.slug === route.params.slug); if (!tag) { await navigateTo('/'); return; }. Alternatively, define a fallback: const tag = Tags.find(...) ?? { language: route.params.slug, slug: route.params.slug, count: 0 }. Also consider asserting in populate.py that every slug written to generated.json has a matching entry in tags.json before both files are written.

36

lowDAT-004Data

No generated output validation before blob upload — empty or near-empty data silently overwrites production

populate.py writes generated.json and immediately exits without asserting that the output contains a minimum number of repositories; sync.js then uploads whatever was written to Vercel Blob without any content check.

Impact

A GitHub API outage, expired token, or network error that causes populate.py to return empty results (rather than raising an exception) would upload a zero-entry generated.json to Vercel Blob and trigger a production deploy of an effectively blank site. There is no prior version to restore from and no alert fires, as noted in ARC-002.

Remediation

After writing generated.json, assert len(REPOSITORIES) >= MIN_THRESHOLD (e.g., 50) before exiting populate.py; raise an exception and exit non-zero if the threshold is not met so the GitHub Actions step fails and sync up is never reached. Add a corresponding check in sync.js or the cron workflow to validate blob file size before triggering the deploy hook.

37

lowSEC-004Security

Two npm devDependencies declared with 'latest' version tag

@nuxt/devtools and @nuxtjs/tailwindcss are listed as 'latest' in package.json, meaning any bun install --no-frozen-lockfile resolves to the then-current published version without a version bound.

Impact

If either package releases a breaking or malicious version and the lockfile is regenerated (e.g. during a CI cache miss or developer setup), the new version installs automatically with no review gate. This is a weaker form of the floating-ref risk that affects the Nuxt build toolchain used to generate production assets.

Remediation

Replace 'latest' with explicit semver ranges matching the currently locked versions (e.g. "@nuxt/devtools": "^1.x.x"). Run bun update to confirm the lock file reflects the pinned range. This does not require upgrading — it only adds a version bound so future updates are intentional.

38

lowSEC-005Security

SubscriptionForm submits user email to undocumented third-party service with no disclosure

SubscriptionForm.vue POSTs directly to https://listmonk.deepsource.io/subscription/form with two hardcoded mailing-list UUIDs; no privacy notice or terms link appears near the form, and the external dependency is undocumented in the README.

Impact

Users submitting their email address are unaware that data flows to a DeepSource-hosted ListMonk instance. If that service changes domain, discontinues, or rejects requests, the form fails silently (native browser POST with no JS error handling). The dependency is invisible to contributors and operators reviewing the repo.

Remediation

Add a one-line disclosure near the form ('Your email will be processed by our newsletter provider') with a link to a privacy policy. Move the ListMonk URL to a NUXT_PUBLIC_LISTMONK_URL build-time environment variable so it is visible in configuration and overridable. Add basic client-side fetch-based form handling so submission errors surface to users. Document the ListMonk dependency in the README alongside other operational dependencies (Vercel Blob, GitHub Actions).

39

lowAIE-003AI Engineering

AI usage policy is advisory only — no enforcement mechanism or PR template

CONTRIBUTING.md requires AI disclosure and code review, but there is no PR template to prompt disclosure, no CI check for AI-related quality signals, and no stated consequence for omission beyond a vague 'will be closed without review.'

Impact

Contributors may skip AI disclosure unintentionally (no PR template to remind them) or submit unverified AI-generated code that passes the minimal CI gate but introduces the pattern issues the policy aims to prevent. The policy provides cover without providing structure.

Remediation

Add a .github/PULL_REQUEST_TEMPLATE.md with a checkbox: '[ ] I used AI tools (Claude, Copilot, etc.) to assist with this PR — I have reviewed and tested all AI-generated code.' This makes disclosure opt-out rather than opt-in and surfaces AI usage in the review interface without adding friction.

40

lowAIE-004AI Engineering

No spec-driven development — pipeline and schema changes lack acceptance criteria

No specs, ADRs, or acceptance criteria exist anywhere in the repository. The most consequential recurring change — adding a repository or modifying the generated JSON schema — has no written definition of done.

Impact

When AI tools or contributors propose changes to the data pipeline contract, there is no reference to validate against. The implicit schema between populate.py and the Vue pages (ARC-001) has no written form to consult, making AI-assisted schema changes especially likely to be incomplete or inconsistent.

Remediation

Add a lightweight data/schema.md (or a TypeScript interface file) that documents the expected shape of generated.json and tags.json. For significant changes, require a brief ADR or updated schema comment before implementation begins. This gives AI tools a stable target to generate against and gives reviewers a concrete contract to check.

41

lowINF-004Infrastructure

Production secret-bearing workflows run on third-party Blacksmith infrastructure rather than GitHub-hosted runners

Both ci.yml and cron.yml specify runs-on: blacksmith-2vcpu-ubuntu-2404; the cron job injects GH_ACCESS_TOKEN, BLOB_READ_WRITE_TOKEN, and DEPLOY_HOOK_URL into a Blacksmith (UseBlacksmith) managed runner environment rather than a GitHub-hosted runner.

Impact

GitHub-hosted runners are managed and audited by GitHub; Blacksmith runners are managed by a third-party provider (UseBlacksmith). Production secrets injected into Blacksmith-managed runner environments are processed outside GitHub's infrastructure trust boundary. While Blacksmith is a legitimate CI acceleration service, this extends the blast radius of any Blacksmith-side compromise to include all three production secrets.

Remediation

Evaluate whether the performance benefit of Blacksmith runners justifies the expanded trust boundary for the cron workflow specifically. If Blacksmith is retained, review UseBlacksmith's security posture and secret isolation guarantees. As a mitigation, scope the Blacksmith runner to the CI workflow only and run the cron workflow (which handles production secrets) on ubuntu-latest (GitHub-hosted). Alternatively, pin all GitHub Actions to SHA references (already recommended in SEC-001) to reduce the supply-chain attack surface on both runner types.

42

lowINF-005Infrastructure

No blob versioning or rollback path — a bad cron run permanently overwrites production data

sync.js uploads generated.json and tags.json with addRandomSuffix: false and no versioned key strategy; the previous generation is overwritten with no snapshot retained and no documented recovery procedure.

Impact

If populate.py produces corrupt or empty output that passes the (absent) quality gate and is uploaded, the previous day's data is unrecoverable from Vercel Blob. Recovery requires re-running the full pipeline from repositories.toml, which depends on GitHub API availability and a valid GH_ACCESS_TOKEN. There is no runbook for this scenario and no maintainer alert fires when it happens (cross-reference ARC-003).

Remediation

Upload generated data to timestamped or run-ID-keyed blob paths (e.g., generated-2026-05-22.json) and maintain a separate pointer file (e.g., latest.json) that is updated only after both files upload successfully. Retain the last N snapshots to enable rollback by updating the pointer. Add a minimum-repository-count assertion in populate.py before the upload step (cross-reference DAT-004) so the overwrite only happens when the pipeline produces valid output. Document the recovery procedure in a runbook or the repository wiki.

43

lowOBS-006Observability

Pipeline logs have no searchable destination or retention beyond 90-day GitHub Actions history

loguru writes to stdout only; all pipeline logs exist solely in GitHub Actions log history, which is capped at 90 days, is not queryable across runs, and cannot be used to correlate errors with specific repository ingestion events.

Impact

If a maintainer needs to investigate why a specific repository stopped appearing in results (e.g., was it rate-limited, archived, or inactive?), they must manually scroll through individual Actions run logs. There is no way to query 'all log lines for repo owner/name across the last 30 pipeline runs'. Log loss after 90 days makes historical debugging impossible.

Remediation

For a project of this scale, the lowest-effort improvement is to capture the populate.py exit status and repo count in a GitHub Actions step summary (echo '### Pipeline result' >> $GITHUB_STEP_SUMMARY; echo "Repos: $REPO_COUNT" >> $GITHUB_STEP_SUMMARY). This provides a per-run dashboard visible in the Actions UI. For more durability, consider writing a one-line summary to a persistent location (e.g., a dated entry in a wiki page or a structured log appended to a Vercel Blob file) after each successful run.

44

lowDEL-005Delivery

GitHub Actions pinned to mutable version tags rather than commit SHAs

actions/checkout@v6, actions/setup-python@v6, astral-sh/setup-uv@v7, and oven-sh/setup-bun@v1 all reference mutable version tags; if the upstream maintainer force-pushes a tag or a tag is hijacked, the CI steps silently change behavior.

Impact

Mutable version tags are a weaker supply-chain guarantee than SHA pinning: a tag can be silently updated to point to a different commit without a pull request or audit trail in this repository. This is a lower-severity issue than the floating @master ref (SEC-001) because named version tags are rarely repointed, but it is still a best-practice gap that leaves CI behavior dependent on external repository history. All affected actions run in the secret-bearing cron workflow, so a compromised tag update would have access to GH_ACCESS_TOKEN, BLOB_READ_WRITE_TOKEN, and DEPLOY_HOOK_URL.

Remediation

Pin each action to its current commit SHA in addition to the readable tag, using the format uses: actions/checkout@<sha> # v6. Look up the SHA for each action's current release (e.g. via git ls-remote or the GitHub Actions marketplace). Add .github/dependabot.yml with a github-actions ecosystem entry on a weekly schedule so Anthropic-style automated SHA-bump PRs keep the pins up to date without manual effort. This also addresses the joelwmale/webhook-action@master issue (SEC-001) if the action is retained rather than replaced with a curl step.

45

lowDOC-005Documentation

No AI context file — AI tools assisting contributors operate with zero project knowledge

The repository contains no CLAUDE.md, AGENTS.md, .cursor/rules, or equivalent; AI tools used by contributors have no awareness of the pipeline invariants, the implicit JSON schema, or the key constraints that prevent regressions.

Impact

AI-assisted contributions to populate.py or Vue components are generated without knowledge of the undocumented JSON contract between the pipeline and the frontend, the SSG build assumptions, or the type-safety gaps already present. This makes AI-generated changes the most likely source of new instances of the structural issues documented in ARC-001. For a project that explicitly acknowledges AI-assisted contributions in CONTRIBUTING.md, providing zero project context to those tools is a missed opportunity to reduce low-quality AI PRs at the source.

Remediation

Add a CLAUDE.md (or AGENTS.md) at the repository root (fewer than 80 lines) covering: project purpose and architecture overview (SSG static site + daily Python pipeline via Vercel Blob), the generated.json/tags.json field contract and that no TypeScript schema exists yet, the fact that Vue script blocks use plain JavaScript (not TypeScript), the test commands to run before submitting (make test), and the things not to do (do not modify generated.json directly, do not use @master CI action refs, do not add env vars without updating .env.example). This file should include an update rule so it stays current as the project evolves.

46

lowDOC-006Documentation

vercel.json documents only two fields — production deployment cannot be reconstructed from the repository

vercel.json contains only buildCommand and framework; required build-time environment variables, custom domain, output directory, and any headers or rewrites are managed exclusively in the Vercel dashboard with no repository record.

Impact

If the Vercel project is deleted, access is revoked, or a new maintainer sets up a fresh Vercel project from the repository, they have no way to know that BLOB_READ_WRITE_TOKEN must be set as a build-time environment variable or that NUXT_PUBLIC_GTAG_ID drives analytics. The first production build attempt would succeed but fetch stale blob data silently (if the token is wrong) or deploy with inactive analytics. This gap is reinforced by INF-002 and has direct documentation-completeness implications.

Remediation

Expand vercel.json to include an env block that lists all required build-time environment variables by name (not value): BLOB_READ_WRITE_TOKEN and NUXT_PUBLIC_GTAG_ID. Add any output directory, headers (X-Frame-Options, CSP), or redirect rules currently configured in the dashboard. Document in CONTRIBUTING.md (or the runbook) that the Vercel project must also have these variables set in the dashboard's Environment Variables section. This makes the Vercel project configuration reconstructable from the repository without dashboard access.