# Changelog

Append-only. One entry per merged step: what shipped, how it was verified, preview URL if any.

## Step 1 — Scaffold (merged bb5ceea)
pnpm workspace (apps/web, packages/detect, packages/sources), Hono Worker with /, /health, cron+queue stubs, wrangler.jsonc bindings, Drizzle+D1, Vitest (Workers runtime via @cloudflare/vitest-plugin), Biome, CI, AGPL-3.0.
Verified: reviewer re-ran install/lint/typecheck/test in a clean worktree and curled the dev server (200/200). Review: PASS, 8 nits; 5 applied here, 3 carried to later steps in PLAN.md.

## Step 2 — Detection library (merged)
`packages/detect`: `detect(files)` + `KNOWN_FILES`; parsers for composer.json/lock, package.json, package-lock (v1-v3), pnpm-lock (v5/v6/v9), bun.lock (JSONC), go.mod, .nvmrc/.node-version/.php-version/.tool-versions, Dockerfile; version normalisation, pin precedence with conflict warnings, deploy rules, framework EOL slugs. Zero I/O. 203 tests, 100% parser line coverage, 47 fixtures (41 real projects, 6 synthetic), 2.1 MB.
Review round 1: FAIL (composer single-pipe OR gave wrong PHP minimum). Fixed + 3 nits (last Dockerfile FROM wins, go toolchain directive, package manager only on successful parse). Round 2: orchestrator re-ran suite + targeted tests, PASS.

## Step 3 — Schema + migrations (merged)
Drizzle schema (Better Auth 1.7.10 tables + teams, team_members, installations, repos, scans, repo_state, findings, digests, notifier_settings), `0000_init.sql`, repository layer incl. `applyFindings` with reopen semantics, local seed, 39 Workers-runtime DB tests.
Review round 1: FAIL — repo identity keyed by name (rename duplicated rows) and digest flag race (lost pendings during send). Fixed: unique (team_id, github_repo_id), `digest_pending_at` timestamp cleared only `<= t0`. Round 2: orchestrator re-ran suite, PASS. Follow-ups to step 6: batch findings writes under D1 limits, scans retention.

## Step 4 — Auth + personal team (merged)
Better Auth 1.7.10 (GitHub only, scopes read:user user:email, encrypted OAuth tokens, unused endpoints disabled), per-request `createAuth(env)`, personal team via `databaseHooks.user.create.after` + race-safe `ensurePersonalTeam` (deterministic id, ON CONFLICT). `/login`, `/login/github`, `/logout`, `/app` shell, Hono JSX layout with Vue-islands slot, `public/app.css` via ASSETS, CSP/X-Frame-Options/nosniff/HSTS, Origin checks. Node-only files on `tsconfig.node.json`. 57 web tests incl. real OAuth callback with mocked GitHub.
Review: PASS; one must-fix was in PLAN.md (callback URL is /api/auth/callback/github). Adversarial run: logout Origin bypasses 403, garbage cookies → /login, open redirect blocked. Follow-up: rate-limit /api/auth/* at step 11.

## Step 5 — GitHub integration (merged)
`packages/sources`: App JWT (RS256 via WebCrypto, PKCS#1→PKCS#8 in code), installation tokens cached in KV, `GitHubClient` (repos list, getRepo, fetchKnownFiles with concurrency 10 / 404-tolerant / >1 MB skip / typed rate-limit errors), webhook HMAC verify. `apps/web`: `/app/github/install` (signed 10-min state), `/github/setup` (installer-only claim via webhook sender record), `/webhooks/github` (signature, 24h dedupe, installation/installation_repositories/push), repo sync, add-by-URL, `/app/repos`, manifest-flow script + guarded callback. 25 + 91 tests.
Review round 1: FAIL — any user with read access could claim an unclaimed org installation. Fixed: only the installer (webhook `sender.id`) may claim; retry page if webhook not yet delivered. Round 2: orchestrator re-verified, PASS. Product finding: App user tokens cannot reach collaborator-only repos → Q41 open.

## Step 6 — Scanner pipeline (merged)
`packages/sources`: endoflife.date client (cycle rules verified live: node major; php/go/symfony/vue major.minor; laravel/next/nuxt/react/angular major; astro/sveltekit untracked), Packagist advisories + Composer constraint matcher, OSV querybatch + details, KV budget (bulk reads, one cache entry per batch, ~6 KV ops for a 1,300-dep repo). `apps/web/src/scan`: scanRepo with TokenProvider, EOL/major_behind/vuln findings, keep-open on source outage or unreadable lockfile (10 MB cap), digest-pending rule, cron 06:00 → scan-team → scan-repo, consumer (batch 5, concurrency 2, retries 3, DLQ), rate-limit deferral with delay, batched applyFindings, scans retention 20, rescan/scan-all routes. 114 + 115 tests.
Review round 1: FAIL — KV per-invocation cap would kill large repos; 1 MB lockfile skip falsely resolved vulns; unbounded consumer concurrency. All fixed + 5 nits. Round 2: orchestrator re-verified, PASS. Note: hosted + self-host require Workers Paid (50 subrequests on Free).

## First production deploy (2026-10-23, after step 6)
Created on the hosted account: D1 `repodar-db`, KV `repodar-cache`, queues `repodar-scan` + `repodar-scan-dlq`. Migration 0000_init applied remotely. Deployed with all 8 secrets via `--secrets-file` (file deleted after). Live at https://repodar.dev: /health 200, /login 200, /app 302→/login, CSP/HSTS/X-Frame-Options present. Crons and queue consumers attached.

## Step 7 — Dashboard UI (merged)
`/app` dashboard (critical-first ordering, summary line, EOL/major-behind chips, declared-minimum marker, prod vuln counts + muted dev count), filter grammar in `src/ui/filter.ts` (ten keys + `vuln:dev|any`, OR same key / AND across keys, dot-segment versions) used server-side and by the Vue island; repo detail (findings grouped, dev + resolved collapsed, deploy override, scan paths, history, two-step delete); settings (owner map, digest email, installation links); Vue 3.5 runtime build vendored (render functions only, CSP-safe), `vendor` script with `--check` in tests. Q42: dev flag in detect for package-lock/composer/pnpm v5-v9 (snapshots graph)/bun. 210 + 114 + 235 tests.
Review round 1: FAIL — pnpm v9 could flag prod packages as dev. Fixed via importer-wide prod reachability. Islands verified in jsdom harness. Round 2: orchestrator re-verified, PASS.

## Step 8 — Notifications (merged)
`LettrClient` (Bearer, Idempotency-Key, 409 in-progress/conflict handling), `Notifier` registry (email only), `computeDigest` diffing against the previous digest's snapshot (New / Escalated / Resolved / Runtime changes / Repos added / removed; dev vulns excluded unless includeDev; 25-per-section cap with filter links; welcome digest first), deterministic HTML+text renderer (inline-styled chips, Gmail-safe), `sendDigests` on the 07:00 cron with the t0 clearing rule, 3-failure stop + banner, snapshot 1 MB guard, 90-row retention; settings: recipient locked to the owner's GitHub email, includeDev, Send test (3/h), Send now, Reset. 126 + 262 tests.
Review: PASS; reviewer's lost/duplicate tests found no lost digest; 6 follow-ups applied before merge.

## Step 8b — Forge integration (merged)
Migration `0001_sources.sql` (team_sources, team_source_observations, repo_state.deploy_source; additive, verified over seeded 0000). `RuntimeSource` interface + registry; `ForgeClient` (servers, sites, deployment-history ≤ 30 sites, phpXY→X.Y, 401/429 handling); tokens sealed with AES-256-GCM (HKDF from BETTER_AUTH_SECRET + team id, provider as AAD); daily `collect-sources` before `scan-team` (DLQ still triggers the scan); observations merged in scanRepo before findings (observed > pinned > ci > declared-minimum; manual deploy_override wins); auto-match by repo name (+branch note), manual map / ignore, unmatched list; settings card with write-only token, Collect now (queued, 60s cooldown), two-step Remove; `source:forge` filter; observed markers on dashboard. 147 + 310 tests.
Review: PASS with one must-fix (dead-lettered collect skipped the daily scan) — fixed, plus 4 nits. Follow-up: Forge v1 API deprecated, migrate when a sunset date appears.

## Hotfix — Forge API v2 (merged, deployed)
Production collect failed with 404: client targeted the deprecated v1 API. Rewritten for `https://forge.laravel.com/api` org-scoped JSON:API endpoints (`GET /orgs` → `/orgs/{slug}/servers` → `/servers/{id}/sites?include=latestDeployment`), with a `GET /sites` fallback when the token lacks `organization:view`. Site `repository` is an object `{provider,url,branch,status}`; PHP falls back to the server's version. Defensive key logging when attributes are missing. 168 sources tests.

## Step 8c — CI sources, OS EOL, activity (merged)
`.github/workflows/*.yml` fetched (≤ 20 files, ≤ 200 KB) and parsed with `yaml` (failsafe schema, alias cap): runtimes from setup-node/php/go incl. matrix max + env resolution (confidence `ci`), deploy hints from `uses:`/`run:` (comments/echo ignored, forge requires curl/wget, PR-only jobs skipped, remote reusable workflows warn). Dockerfile base-image OS → endoflife.date `debian`/`ubuntu`/`alpine-linux` EOL findings (`eol:os:*`); implicit-distro notes demoted to info. `repos.last_pushed_at`, stale/archived chips, `activity:` filter, `server`/`fly` targets. Migration `0002_repo_activity.sql` (3 additive columns incl. `repo_state.notes`, `repo_state.os`). 340 + 159 + 333 tests.
Review: code PASS; FAIL on fixture hygiene (real AWS account id, S3 bucket names) — scrubbed, branch squashed before merge; older step-2 fixtures also scrubbed. History squash scheduled for step 11 before going public.
Real-world findings: app.topol CI on Node 20 (EOL), ecomailweb-be CI on Node 16 (EOL), benefity declares PHP ≥8.3 but CI runs 8.4.

## Step 8d — API sources + GitHub PAT (merged)
Cloudflare (accounts → Workers scripts + Builds triggers + Pages projects), Vercel (teams → v10 projects: nodeVersion observed, link.repo, latest deployment), DigitalOcean (apps → components github.repo/branch) as RuntimeSources with the Forge pattern (encrypted tokens, matched/unmatched, manual map/ignore, Collect now per provider, two-step Remove). Fine-grained GitHub PAT card (Q41): refuses non-`github_pat_` tokens, validates via /user + /rate_limit, reachability table per URL-added repo (>20 → queued), TokenProvider order installation → PAT → owner token. Repos page: "Manage repos on GitHub" (user/org URLs, no-JS list for 2+), "Install on another account" secondary. No migration. 192 + 371 tests.
Review: FAIL on one item (classic tokens accepted with warning) → refused; 5 nits applied. Reviewer's own token-leak (401/403/429/500 echoing the token), IDOR (foreign observation/repo ids) and routing tests all passed.

## Step B1 — Brand kit (merged)
`brand/`: README (positioning, tagline "A quiet radar over every repo you maintain.", voice with 10 UI strings, logo rules, color system Night/Radar/Dusk/Moth with semantic severities, WCAG table min 5.53:1 text / 3.54:1 non-text, Bricolage Grotesque OFL display face vendored, Echo construction + pose→use-case map, Tailwind @theme mapping, email-safe hex list), `tokens.css`/`tokens.json`, logo mark/wordmark/lockups (light/dark/mono), Echo ×6 + head glyph, favicon set + manifest, OG 1200×630, `build.mjs` (rasterise, contrast inject, SVG validate, inventory check) wired into `pnpm test` as `brand:check`. Orchestrator visually approved; review PASS, 5 nits applied.

## Step 8e — GitHub Deployments (merged)
`github-deployments` RuntimeSource using each repo's own token: deployments list + statuses (≤ 2 requests/repo typical, ≤ 4 cap, 1 when unchanged), per-environment last success, `vercel[bot]` → target labelled `hint`; platform API `lastDeployedAt` wins, GitHub fills gaps ("also seen on GitHub"). Missing `deployments: read` → probe repo's 403 records `permission_missing` once per installation, card explains the exact steps, flips to Active automatically. Every team gets collect-before-scan daily; 400-request budget per team with cursor. 196 + 387 tests. Review: PASS; 6 follow-ups applied.

## Step 9 — Billing behind flag + cost guardrails (merged)
`billingMode(env)`: on only when SELF_HOSTED!=="true" AND BILLING_ENABLED==="true" (default "false" in both envs). Off-mode: `/billing/*` + `/webhooks/stripe` 404, no pricing/CTA strings, unlimited teams, Stripe secrets not required, no Stripe hosts in CSP. On-mode: form-encoded Stripe Checkout/Portal, webhook (t/v1 HMAC, 300 s tolerance, constant-time, event-id replay guard, created-ordering, multi-subscription safe), Free cap 25 active repos (sync alphabetical fill + banner, add-by-URL CTA), Pro unlimited. All modes: 12 push/manual scans per repo per UTC day, 500 per team, cron uncapped, one summary log line per scan. 426 web tests.
Review: FAIL on a webhook downgrade bug via a different subscription id + rebase break → fixed with 4 nits. Off-mode verified airtight with flag variants and HTML greps.

## Step 11a — Rate limiting (merged)
Fixed-window KV limiter (`src/middleware/rate-limit.tsx`): /api/auth/* 30/10 min per IP (get-session exempt, 0 KV ops), POST /login/github 10/10 min, /github/setup 20/10 min, manifest callback 5/10 min, add-by-URL 30/10 min per team, all POST /app/* 600/min per team backstop, failed webhook signatures 60/10 min per IP (valid deliveries never counted). 429 with Retry-After + RateLimit headers; one log line per first block per window; `RATE_LIMIT_DISABLED` only in test bindings. 16 tests. Orchestrator-verified (small, self-contained); no separate review round.

## Step 10 — README, docs, CI hardening (merged)
README (quickstart, privacy, self-host pointer), `docs/` (self-host, sources, filter-syntax, architecture, security incl. rate limits), `.github/` (issue forms, PR template, SECURITY.md, CODEOWNERS, grouped Dependabot), CI: actions pinned to verified SHAs, `cf-typegen --check`, `docs:check` in `pnpm test`; `deploy.yml` on push to main: CI → migrations → deploy, inert until `CLOUDFLARE_API_TOKEN` exists. Review: PASS after one fix (secrets file path outside repo + .gitignore guard).

## Step B2 — App redesign (merged)
Tailwind v4 (pinned CLI, static `public/app.css` 46.8 KB via `vendor`, `--check` guards drift) + Basecoat 1.0.2 CSS-only component vocabulary, brand tokens mapped via `@theme`; Echo poses inlined from generated `src/ui/echo.ts`; Bricolage Grotesque self-hosted for headlines; all pages restyled (login, dashboard, detail, repos, settings Sources cards, confirm, 404/500); digest email on email-safe hex with Echo head; one severity map (`src/ui/severity.ts`) for chips/badges/digest — brand README severity table corrected to scanner semantics; CSP unchanged; axe 0 violations on 13 pages; screenshots script. 444 web + 34 Node tests.
Review round 1: FAIL — login text column collapsed (Basecoat container-type header), phone overflow (`.table`/`.filter` Tailwind utility collisions), severity mismatch between dashboard and detail; all fixed. Orchestrator inspected screenshots (desktop light/dark, 360/390/551 phone) before merge.

## Step B3 — Marketing site + data layer (merged)
Landing (`/`, Echo brand, dashboard mock `aria-hidden`+`inert`, 6-question FAQ, "Free while in beta"), `/privacy`, `/terms`, `/security`, `/changelog`, `/docs/*` rendered from Markdown at build time through a sanitiser (no `marked`/`jsdom` in the Worker bundle; collision-safe heading ids), `sitemap.xml` (11 pages), `robots.txt`, JSON-LD (SoftwareApplication, Organization, FAQPage), OG image, canonical without query, `NOINDEX` mode, `llms.txt`/`llms-full.txt`, `Accept: text/markdown` and `.md` negotiation with `Vary: Accept`. Analytics all OFF without keys: Cloudflare Web Analytics (`CF_WEB_ANALYTICS_TOKEN`, validated 32-hex, CSP extended only then) and PostHog (`POSTHOG_KEY`, consent banner island, `rd_consent` HttpOnly cookie, `/consent` with Origin check and safe return path, `/ph/*` proxy: consent-gated, allowlisted hosts, headers/cookies stripped, 413 on >1 MB, 10 s upstream timeout → 504). Digest preview iframe with own CSP. Hashed `/assets/app.<hash>.css` immutable, bare `/app.css` 1 day, marketing pages `max-age=300`. Lighthouse mobile 100/100/100/100 on `/` and `/login`; CSS 56.6 KB (budget 60 KB). Legal pages carry placeholders for the user to fill. Tests: web 496 Worker + 64 Node, detect 340, sources 196.
Review: PASS. Must-fixes (proxy timeout, llms.txt summary casing) and nits (heading ids, 320 px iframe clip, `signup_completed` via waitUntil, `/ph/array/*` to assets host, meta description ≤160, signed-out 404 link, privacy wording, `_headers` caching, PLAN CSS budget) applied; orchestrator re-verified in a clean worktree (lint, typecheck, 1,096 tests, vendor/brand/docs checks).

## Step 12 — Exact runtime versions (merged)
Rule: a runtime version is shown as precisely as its source states it (`.php-version` `8.5.12`, `FROM php:8.5.12-fpm`, `.nvmrc` `22.11.0`, `~8.3.1` → `≥ 8.3.1`); EOL, major-behind, severity and finding identities stay keyed on the major.minor `cycle`. `Runtime` gains `cycle`; rows stored before this step are backfilled on read, so the first scan after deploy reopens or closes nothing and sends no digest line. Forge only reports `php85`, so the Forge source now sends one `HEAD` request per public site domain, at most once a day, no redirects followed, and accepts `X-Powered-By`/`Server` `PHP/x.y.z` only when it agrees with Forge's major.minor; cap 60 probes per collect (≤ 300 per 5-team queue batch), 4 in flight, 15 s deadline, never-probed sites first. When Forge has no patch level, an agreeing `.php-version` pin or CI setup step supplies it (header > pin > CI > Forge). Chips, tooltips, repo page, settings site list, digest lines and the `php:8.5` filter (component-prefix, matches `8.5.12` not `8.50`) all follow the rule. Docs: sources (what is sent, what is stored, `expose_php`), filter syntax. Tests: detect 374, sources 196, web 553 Worker + 64 Node.
Review: FAIL round 1 — a 5-team batch with followed redirects could pass the 1,000-subrequest limit, sites were re-probed on every collect, and the documented pin precision was lost in the observed merge. All fixed plus nits (header normalisation, probe failures never fail the collect, private-domain list, shared-domain probes, screenshots script without `.dev.vars`); orchestrator re-verified in a clean worktree.

## Step 13 — Rescan collects sources first (merged)
Manual Rescan (one repo) and Scan all now enqueue `collect-sources` first and scan afterwards, so one button refreshes runtime sources (Forge with its once-a-day header probe, GitHub Deployments, Cloudflare, Vercel, DigitalOcean) and then the repo. Gated by a shared per-team manual collect window of 5 minutes in KV (`claimManualCollect`), also honoured by Settings "Collect now" and GitHub Deployments "Check again"; inside the window, or when a scan is in flight, the repo is capped, or the team has no sources, the old direct scan runs. Follow-up scans carry `trigger: "manual"` and count against the 12/repo and 500/team daily caps; requested repos are deduplicated against repos the collect already changed, and repo ids from another team are ignored. A Rescan-triggered collect reads GitHub Deployments only for the rescanned repo (1–4 requests instead of one per team repo), keeping other repos' observations and the resume cursor intact. Daily cron and push paths are unchanged. Notices: "Checking your sources, then rescanning. Results appear here in a minute or so." Docs: sources, architecture. Also made the install-state tamper test deterministic (it passed by chance about 1 run in 256). Tests: detect 374, sources 196, web 560 Worker + 64 Node.
Review: PASS; cost nit (team-wide Deployments read per Rescan) and tooltip wording fixed before merge; orchestrator re-verified in a clean worktree.

## Step 14 — Server runtime reports (merged)
Forge's API only knows PHP minors and the user's servers hide `X-Powered-By`, so the server now tells Repodar itself. Settings → "Server reports": per-team tokens (`rdr_…`, 32 random bytes, SHA-256 hash only in the new `team_report_tokens` table, shown once, max 10 active, revocable, one-off form key so a reload creates nothing). `POST /ingest/runtime` (Bearer token, JSON ≤ 4 KB: `site`, `host`, `repo`, `runtimes`; 202/400/401/413/429; per-token, per-team and per-IP limits with the IP bucket checked before any hashing; `no-store`; no session, no CORS preflight; nothing sensitive logged). Reports are `observed` runtimes with the highest precedence (report > Forge header probe > pin > CI > Forge minor), fresh for 48 h, matched by `repo` hint or through the team's Forge sites; several hosts for one repo merge deterministically (lowest version wins, all hosts named in the tooltip) and only a change in the merged result enqueues one coalesced, capped rescan; a disagreement with Forge shows a calm note on the repo page. Copy-button snippets for the Forge deploy script (`$FORGE_SITE_PATH`, `$FORGE_PHP`), a daily Forge scheduler job and a generic cron line, all `|| true` so an outage never fails a deploy. Filter `source:report`. Migration `0003_report_tokens` (additive, `IF NOT EXISTS`). Docs: sources, security, architecture, filter syntax. Tests: detect 374, sources 196, web 622 Worker + 64 Node.
Review: FAIL round 1 — settings page scrolled sideways (snippet `pre` in an auto-sized grid column) and two load-balanced hosts reporting different patches caused a rescan per report; fixed plus nits (cron stderr, failed-token order, hourly `last_used_at`, atomic token cap, stricter site/host/repo validation). Hands-on endpoint testing (prototype pollution, 10 MB bodies, header spoofing, XSS via host names, cross-team tokens) held. Orchestrator re-verified in a clean worktree.

## Step 15 — Exact versions from the server, for every language (merged)
Forge can run a command on the server for us, so no pasting: once a day per PHP site (and with the token scope `site:manage-commands`), Repodar asks Forge to run one fixed POSIX line that prints `name=version` for the site's PHP binary and for node, bun, deno, go, python, ruby, java, dotnet and rust when found on PATH; nothing in that line comes from user input (only `phpX.Y` through a `^\d\.\d{1,2}$` allowlist). A delayed `forge-command-output` queue message reads the output (~1, 3, 8, 13 min; `filter[command]` + `sort=-created_at`, exact text match, not older than the create), accepts PHP only when it agrees with Forge's minor, stores the rest as `commandRuntimes`. Caps: 60 creates per collect, once per 20 h per site, never-asked first, 15 s deadline, 429 stops, per-team KV lease against concurrent collects; 403 → 24 h backoff and a calm scope hint on the Forge card ("Exact PHP versions: on · last run … · N sites" when working). Precedence: server report > Forge command > header probe > pin > CI > Forge minor. For servers not on Forge: `GET /report.sh`, a POSIX script served verbatim (SHA-256 published in docs via vendor), always exits 0, token via env or `~/.repodar/token` passed to curl on stdin, finds sites (`$FORGE_SITE_PATH`, cwd, `/home/*/<name.tld>`, nginx `server_name`) and each site's PHP from its nginx `fastcgi_pass` socket, reports machine-level runtimes for all ten names; `--dry-run`, `--site`, `-h`. Endpoint batch shape (≤ 50 sites, ≤ 16 KB, machine-level fallback, `rejected` list, 202 `{matched, unmatched}`), step 14 single shape kept. `RUNTIME_NAMES` (10) and `DETECTED_RUNTIMES` (php, node, go) defined once in packages/detect; observed non-PHP runtimes refine runtimes the repo already uses, otherwise appear on the repo page under "Also on this server" (chips/EOL for the other languages arrive in step 16). Settings: Forge card first; the reports card collapsed under "Other servers (AWS, DigitalOcean, bare metal)". Docs: sources, security (`curl | sh` posture, what the Forge command runs), self-host, architecture. Tests: detect 374, sources 201, web 653 Worker + 86 Node.
Review: FAIL round 1 — settings copy said only `php -r 'echo PHP_VERSION;'` runs; fixed, plus nits (first-line-per-runtime parsing, invalid host runtimes rejected not fatal, `set -f`, localhost check, per-file nginx PHP lookup, lease + merge-only writes, timing docs, scope link). Orchestrator re-verified in a clean worktree.

## Step 16 — Every language end to end (merged)
All ten runtimes (php, node, bun, deno, go, python, ruby, java, dotnet, rust) now have detection, chips, EOL, filters and digest lines under the step 12 rules (precise version, `cycle` keys EOL/severity/identity; observed > pinned > ci > declared-minimum). New parsers: `.python-version`, `pyproject.toml` (`requires-python`, Poetry incl. inline form), `runtime.txt`, `Pipfile`, `.ruby-version`, `Gemfile` ruby, `Gemfile.lock` RUBY VERSION, `.java-version`, `.sdkmanrc`, `pom.xml` (property indirection; unresolved `${prop}` warns), `build.gradle(.kts)`, `global.json` (SDK pin), root `*.csproj` TFMs, `rust-toolchain(.toml)`, `Cargo.toml` `rust-version` (MSRV: chip only), `.bun-version`, `packageManager`/`engines.bun`, `.dvmrc`, `.tool-versions` for all, Dockerfile images (python, ruby, eclipse-temurin, dotnet, rust, oven/bun, denoland/deno), CI setup actions (python matrix, java, dotnet `8.0.x`, dtolnay/setup-rust, setup-bun, setup-deno); release channel words (`stable`, `latest`, `nightly`, `lts/*`) are notes. Lockfile dependencies for `requirements.txt` (`==` only), `poetry.lock`, `uv.lock`, `Gemfile.lock`, `Cargo.lock` → OSV PyPI, RubyGems, crates.io. Frameworks: Django, Rails, Spring Boot. EOL products verified live 2026-10-24: python, ruby, eclipse-temurin (java `1.8` → 8), dotnet (mixed major / major.minor), bun, deno (2.x per minor, 1.x as `1`); Rust and Deno 2 get a "releases behind" finding (`major_behind:<name>`, ≥4/≥8 medium/high for Rust, ≥2/≥4 for Deno 2) instead of a false critical EOL; php/node/go finding keys byte-identical (tested). Hostile input: XML (`src/xml.ts`) and TOML readers are linear (30 one-MB repetition cases each < 200 ms; XML > 1 MB skipped with a note). Fetch cost: each scanned folder is listed once and only known files that exist are fetched (`.do`/`.github` only when listed, > 10 MB skipped unfetched, ≥ 1,000 entries or a failed listing → blind fallback); file requests per repo across 56 fixtures: 22.3 on main → 4.7. Docs: sources (runtime table), filter syntax, README, landing. Tests: detect 569 (99.5 % parser line coverage), sources 231, web 709 Worker + 86 Node.
Review: FAIL round 1 — quadratic regexes on unclosed XML (1 MB pom.xml 38 s), quadratic TOML array copy, Rust/Deno 2 EOL noise, 23 blind file fetches per repo, channel words as warnings, silent unresolved pom properties; all fixed plus nits (help text, csproj root-only note, Poetry inline form, Pipfile pin precision, SDK tooltip). Orchestrator re-verified in a clean worktree and removed one unused test variable.
