diff --git a/.claude/skills/contributing-to-gittensory/SKILL.md b/.claude/skills/contributing-to-gittensory/SKILL.md index 1664e458e1..31641122d9 100644 --- a/.claude/skills/contributing-to-gittensory/SKILL.md +++ b/.claude/skills/contributing-to-gittensory/SKILL.md @@ -201,7 +201,7 @@ Run the matching command(s) and **commit the regenerated file(s)** — CI fails | A Cloudflare binding/var in `wrangler.jsonc` | `npm run cf-typegen` | `worker-configuration.d.ts` | | Drizzle schema (`src/db/schema.ts`) | `npm run drizzle:generate` | the new `migrations/NNNN_*.sql` | | Added a raw-SQL migration | (none — just author it) | next **contiguous** `migrations/NNNN_snake.sql` | -| `src/selfhost/**` (or a few other scanned files — see `scripts/gen-selfhost-env-reference.mjs`'s `DEFAULT_SOURCE_ROOTS`) touching an `env.SOMETHING` read | `npm run selfhost:env-reference` | `apps/gittensory-ui/src/lib/selfhost-env-reference.ts` — the doc embeds `file:line` citations, so even an edit that only shifts line numbers (no var added/removed) goes stale | +| `src/selfhost/**` (or a few other scanned files — see `scripts/gen-selfhost-env-reference.mjs`'s `DEFAULT_SOURCE_ROOTS`) adding/removing an `env.SOMETHING` read | `npm run selfhost:env-reference` | `apps/gittensory-ui/src/lib/selfhost-env-reference.ts` — the doc cites the file only (not `file:line`, deliberately, so an unrelated line shift elsewhere in the file never makes this go stale) | | CLI command surface | `npm run command-reference` | the generated command-reference doc | | UI files (`apps/gittensory-ui/**`) | `npm --workspace @jsonbored/gittensory-ui run format` | formatted files | diff --git a/.claude/skills/contributing-to-gittensory/reference.md b/.claude/skills/contributing-to-gittensory/reference.md index 0babcd07ff..1a2de0aabf 100644 --- a/.claude/skills/contributing-to-gittensory/reference.md +++ b/.claude/skills/contributing-to-gittensory/reference.md @@ -28,7 +28,7 @@ path filter matched; on push to `main`, everything runs. | lint → migrations | migration guard | `npm run db:migrations:check` | duplicate/gap/misnamed migration number | | lint → cf-typegen | worker types drift | `npm run cf-typegen:check` | committed `worker-configuration.d.ts` is stale (run `npm run cf-typegen`) | | lint → schema-drift | `src/db/schema.ts` vs `migrations/` | `npm run db:schema-drift:check` | a Drizzle table's schema doesn't match the migration history | -| lint → selfhost-env-reference | self-host env-var doc drift | `npm run selfhost:env-reference:check` | committed `apps/gittensory-ui/src/lib/selfhost-env-reference.ts` is stale (run `npm run selfhost:env-reference`) — triggers on ANY edit to `src/selfhost/**` (+ a few other scanned files) that adds/removes an env var read OR just shifts line numbers of an existing one, since the doc embeds `file:line` citations | +| lint → selfhost-env-reference | self-host env-var doc drift | `npm run selfhost:env-reference:check` | committed `apps/gittensory-ui/src/lib/selfhost-env-reference.ts` is stale (run `npm run selfhost:env-reference`) — triggers when `src/selfhost/**` (+ a few other scanned files) adds/removes an env var read; a pure line shift of an existing read does NOT trigger it, since the doc cites the file only, not `file:line` (#env-reference-churn) | | lint → observability | Grafana/Prometheus/alert config validation | `npm run selfhost:validate-observability` | a self-host observability config (dashboard/rule/datasource) is malformed | | lint → typecheck | `tsc --noEmit` | `npm run typecheck` | any backend type error | | test (1/2) | sharded vitest + coverage | `npm run test:coverage` (unsharded) | any failing `test/**/*.test.ts` (excl. `test/workers/**`) | diff --git a/AGENTS.md b/AGENTS.md index 3d1efbfba0..d764047437 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ process evolves — edits to those files improve both Claude Code and Codex. under-reports). 4. **Regenerate and commit generated artifacts:** `npm run ui:openapi` (API/schema changes), `npm run cf-typegen` (wrangler binding/var changes), `npm run selfhost:env-reference` (any - `env.SOMETHING` read added/removed/line-shifted under `src/selfhost/**`), and a contiguous + `env.SOMETHING` read added/removed under `src/selfhost/**`), and a contiguous `migrations/NNNN_*.sql` (DB changes). Stale generated files fail CI (see `reference.md`'s full check table for the complete list — `test:ci` runs more drift checks than these three). 5. **House rules:** Conventional Commits, **no AI/Claude/agent attribution** in commits or PR text; diff --git a/apps/gittensory-ui/src/lib/selfhost-env-reference.ts b/apps/gittensory-ui/src/lib/selfhost-env-reference.ts index 422ff91115..f4199e26cd 100644 --- a/apps/gittensory-ui/src/lib/selfhost-env-reference.ts +++ b/apps/gittensory-ui/src/lib/selfhost-env-reference.ts @@ -7,487 +7,487 @@ export type SelfHostEnvReferenceRow = { export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [ { name: "AI_COMBINE", - firstReference: "src/selfhost/ai.ts:1149", + firstReference: "src/selfhost/ai.ts", }, { name: "AI_DUAL_REVIEW", - firstReference: "src/selfhost/ai.ts:1151", + firstReference: "src/selfhost/ai.ts", }, { name: "AI_EMBED_API_KEY", - firstReference: "src/server.ts:443", + firstReference: "src/server.ts", }, { name: "AI_EMBED_BASE_URL", - firstReference: "src/server.ts:440", + firstReference: "src/server.ts", }, { name: "AI_EMBED_MODEL", - firstReference: "src/selfhost/ai.ts:1045", + firstReference: "src/selfhost/ai.ts", }, { name: "AI_ON_MERGE", - firstReference: "src/selfhost/ai.ts:1149", + firstReference: "src/selfhost/ai.ts", }, { name: "AI_PROVIDER", - firstReference: "src/selfhost/ai-config.ts:43", + firstReference: "src/selfhost/ai-config.ts", }, { name: "ANTHROPIC_AI_BASE_URL", - firstReference: "src/selfhost/ai.ts:1049", + firstReference: "src/selfhost/ai.ts", }, { name: "ANTHROPIC_AI_MODEL", - firstReference: "src/selfhost/ai.ts:109", + firstReference: "src/selfhost/ai.ts", }, { name: "ANTHROPIC_API_KEY", - firstReference: "src/selfhost/ai.ts:1048", + firstReference: "src/selfhost/ai.ts", }, { name: "BACKUP_ACKNOWLEDGED", - firstReference: "src/server.ts:381", + firstReference: "src/server.ts", }, { name: "BROWSER_WS_ENDPOINT", - firstReference: "src/selfhost/stubs/puppeteer.ts:11", + firstReference: "src/selfhost/stubs/puppeteer.ts", }, { name: "CLAUDE_AI_EFFORT", - firstReference: "src/selfhost/ai.ts:160", + firstReference: "src/selfhost/ai.ts", }, { name: "CLAUDE_AI_MODEL", - firstReference: "src/selfhost/ai.ts:101", + firstReference: "src/selfhost/ai.ts", }, { name: "CLAUDE_AI_TIMEOUT_MS", - firstReference: "src/selfhost/ai.ts:160", + firstReference: "src/selfhost/ai.ts", }, { name: "CODEX_AI_EFFORT", - firstReference: "src/selfhost/ai.ts:164", + firstReference: "src/selfhost/ai.ts", }, { name: "CODEX_AI_FIRST_OUTPUT_TIMEOUT_MS", - firstReference: "src/selfhost/ai.ts:184", + firstReference: "src/selfhost/ai.ts", }, { name: "CODEX_AI_MODEL", - firstReference: "src/selfhost/ai.ts:105", + firstReference: "src/selfhost/ai.ts", }, { name: "CODEX_AI_TIMEOUT_MS", - firstReference: "src/selfhost/ai.ts:164", + firstReference: "src/selfhost/ai.ts", }, { name: "CODEX_HOME", - firstReference: "src/selfhost/ai.ts:353", + firstReference: "src/selfhost/ai.ts", }, { name: "CRON_INTERVAL_MS", - firstReference: "src/server.ts:922", + firstReference: "src/server.ts", }, { name: "DATABASE_PATH", - firstReference: "src/server.ts:251", + firstReference: "src/server.ts", }, { name: "DATABASE_URL", - firstReference: "src/selfhost/preflight.ts:201", + firstReference: "src/selfhost/preflight.ts", }, { name: "DISCORD_REPO_WEBHOOKS", - firstReference: "src/services/notify-discord.ts:41", + firstReference: "src/services/notify-discord.ts", }, { name: "DISCORD_WEBHOOK_URL", - firstReference: "src/services/notify-discord.ts:78", + firstReference: "src/services/notify-discord.ts", }, { name: "FOREGROUND_LIVENESS_CHECK_INTERVAL_MS", - firstReference: "src/selfhost/foreground-liveness.ts:52", + firstReference: "src/selfhost/foreground-liveness.ts", }, { name: "FOREGROUND_LIVENESS_ENABLED", - firstReference: "src/selfhost/foreground-liveness.ts:41", + firstReference: "src/selfhost/foreground-liveness.ts", }, { name: "FOREGROUND_LIVENESS_MAX_DEFER_MS", - firstReference: "src/selfhost/foreground-liveness.ts:51", + firstReference: "src/selfhost/foreground-liveness.ts", }, { name: "FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP", - firstReference: "src/selfhost/foreground-liveness.ts:53", + firstReference: "src/selfhost/foreground-liveness.ts", }, { name: "GITHUB_APP_ID", - firstReference: "src/selfhost/orb-collector.ts:59", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "GITHUB_APP_PRIVATE_KEY", - firstReference: "src/selfhost/orb-collector.ts:166", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "GITHUB_CACHE_TTL_SECONDS", - firstReference: "src/server.ts:512", + firstReference: "src/server.ts", }, { name: "GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS", - firstReference: "src/selfhost/installation-concurrency-admission.ts:47", + firstReference: "src/selfhost/installation-concurrency-admission.ts", }, { name: "GITHUB_INSTALLATION_CONCURRENCY_ENABLED", - firstReference: "src/selfhost/installation-concurrency-admission.ts:34", + firstReference: "src/selfhost/installation-concurrency-admission.ts", }, { name: "GITHUB_INSTALLATION_CONCURRENCY_LIMIT", - firstReference: "src/selfhost/installation-concurrency-admission.ts:43", + firstReference: "src/selfhost/installation-concurrency-admission.ts", }, { name: "GITTENSORY_REPO_CONFIG_DIR", - firstReference: "src/server.ts:290", + firstReference: "src/server.ts", }, { name: "GITTENSORY_VERSION", - firstReference: "src/selfhost/otel.ts:62", + firstReference: "src/selfhost/otel.ts", }, { name: "HOME", - firstReference: "src/selfhost/ai.ts:353", + firstReference: "src/selfhost/ai.ts", }, { name: "MAINTENANCE_ADMISSION_DEFER_MS", - firstReference: "src/selfhost/maintenance-admission.ts:171", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_DRAIN_AGE_MS", - firstReference: "src/selfhost/maintenance-admission.ts:145", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_ENABLED", - firstReference: "src/selfhost/maintenance-admission.ts:126", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING", - firstReference: "src/selfhost/maintenance-admission.ts:167", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS", - firstReference: "src/selfhost/maintenance-admission.ts:141", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS", - firstReference: "src/selfhost/maintenance-admission.ts:155", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_MAX_LIVE_PENDING", - firstReference: "src/selfhost/maintenance-admission.ts:151", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MAINTENANCE_ADMISSION_MAX_PENDING", - firstReference: "src/selfhost/maintenance-admission.ts:159", + firstReference: "src/selfhost/maintenance-admission.ts", }, { name: "MIGRATIONS_DIR", - firstReference: "src/server.ts:395", + firstReference: "src/server.ts", }, { name: "OBSERVABILITY_SMOKE_POLL_MS", - firstReference: "scripts/smoke-observability-traces.mjs:8", + firstReference: "scripts/smoke-observability-traces.mjs", }, { name: "OBSERVABILITY_SMOKE_TIMEOUT_MS", - firstReference: "scripts/smoke-observability-traces.mjs:6", + firstReference: "scripts/smoke-observability-traces.mjs", }, { name: "OLLAMA_AI_API_KEY", - firstReference: "src/selfhost/ai.ts:1042", + firstReference: "src/selfhost/ai.ts", }, { name: "OLLAMA_AI_BASE_URL", - firstReference: "src/selfhost/ai.ts:1038", + firstReference: "src/selfhost/ai.ts", }, { name: "OLLAMA_AI_MODEL", - firstReference: "src/selfhost/ai.ts:113", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_AI_BASE_URL", - firstReference: "src/selfhost/ai.ts:1040", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_AI_MODEL", - firstReference: "src/selfhost/ai.ts:114", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_API_KEY", - firstReference: "src/selfhost/ai.ts:1042", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_COMPATIBLE_AI_API_KEY", - firstReference: "src/selfhost/ai.ts:1042", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_COMPATIBLE_AI_BASE_URL", - firstReference: "src/selfhost/ai.ts:1041", + firstReference: "src/selfhost/ai.ts", }, { name: "OPENAI_COMPATIBLE_AI_MODEL", - firstReference: "src/selfhost/ai.ts:115", + firstReference: "src/selfhost/ai.ts", }, { name: "ORB_AIR_GAP", - firstReference: "src/selfhost/orb-collector.ts:161", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_ANONYMIZE", - firstReference: "src/selfhost/orb-collector.ts:174", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_APP_ID", - firstReference: "src/selfhost/orb-collector.ts:59", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_BROKER_URL", - firstReference: "src/server.ts:977", + firstReference: "src/server.ts", }, { name: "ORB_COLLECTOR_TOKEN", - firstReference: "src/selfhost/orb-collector.ts:205", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_COLLECTOR_URL", - firstReference: "src/selfhost/orb-collector.ts:172", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_ENROLLMENT_SECRET", - firstReference: "src/selfhost/orb-collector.ts:165", + firstReference: "src/selfhost/orb-collector.ts", }, { name: "ORB_RELAY_MODE", - firstReference: "src/server.ts:965", + firstReference: "src/server.ts", }, { name: "OTEL_EXPORTER_OTLP_ENDPOINT", - firstReference: "src/selfhost/otel.ts:47", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - firstReference: "src/selfhost/otel.ts:45", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_SERVICE_ENVIRONMENT", - firstReference: "src/selfhost/otel.ts:60", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_SERVICE_NAME", - firstReference: "src/selfhost/otel.ts:59", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_TRACES_EXPORTER", - firstReference: "src/selfhost/otel.ts:40", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_TRACES_SAMPLER", - firstReference: "src/selfhost/otel.ts:74", + firstReference: "src/selfhost/otel.ts", }, { name: "OTEL_TRACES_SAMPLER_ARG", - firstReference: "src/selfhost/otel.ts:76", + firstReference: "src/selfhost/otel.ts", }, { name: "PGPOOL_MAX", - firstReference: "src/selfhost/queue-common.ts:713", + firstReference: "src/selfhost/queue-common.ts", }, { name: "PGVECTOR_ENABLED", - firstReference: "src/server.ts:231", + firstReference: "src/server.ts", }, { name: "PORT", - firstReference: "src/server.ts:721", + firstReference: "src/server.ts", }, { name: "PUBLIC_API_ORIGIN", - firstReference: "src/selfhost/preflight.ts:192", + firstReference: "src/selfhost/preflight.ts", }, { name: "QDRANT_API_KEY", - firstReference: "src/selfhost/qdrant-vectorize.ts:50", + firstReference: "src/selfhost/qdrant-vectorize.ts", }, { name: "QDRANT_DIM", - firstReference: "src/selfhost/qdrant-vectorize.ts:71", + firstReference: "src/selfhost/qdrant-vectorize.ts", }, { name: "QDRANT_URL", - firstReference: "src/server.ts:531", + firstReference: "src/server.ts", }, { name: "QUEUE_BACKGROUND_CONCURRENCY", - firstReference: "src/selfhost/queue-common.ts:130", + firstReference: "src/selfhost/queue-common.ts", }, { name: "QUEUE_CONCURRENCY", - firstReference: "src/selfhost/pg-queue.ts:286", + firstReference: "src/selfhost/pg-queue.ts", }, { name: "QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS", - firstReference: "src/selfhost/queue-common.ts:721", + firstReference: "src/selfhost/queue-common.ts", }, { name: "QUEUE_STARTUP_JITTER_MIN_JOBS", - firstReference: "src/selfhost/queue-common.ts:702", + firstReference: "src/selfhost/queue-common.ts", }, { name: "REDIS_URL", - firstReference: "src/selfhost/preflight.ts:144", + firstReference: "src/selfhost/preflight.ts", }, { name: "REVIEW_AUDIT_DIR", - firstReference: "src/server.ts:576", + firstReference: "src/server.ts", }, { name: "SELFHOST_BUNDLE_ALL", - firstReference: "scripts/build-selfhost.mjs:13", + firstReference: "scripts/build-selfhost.mjs", }, { name: "SELFHOST_SERVICE", - firstReference: "scripts/smoke-observability-traces.mjs:5", + firstReference: "scripts/smoke-observability-traces.mjs", }, { name: "SELFHOST_SETUP_TOKEN", - firstReference: "src/selfhost/preflight.ts:186", + firstReference: "src/selfhost/preflight.ts", }, { name: "SENTRY_DSN", - firstReference: "src/selfhost/sentry.ts:389", + firstReference: "src/selfhost/sentry.ts", }, { name: "SENTRY_ENVIRONMENT", - firstReference: "src/selfhost/otel.ts:60", + firstReference: "src/selfhost/otel.ts", }, { name: "SENTRY_RELEASE", - firstReference: "src/selfhost/otel.ts:62", + firstReference: "src/selfhost/otel.ts", }, { name: "SENTRY_SERVER_NAME", - firstReference: "src/selfhost/sentry.ts:407", + firstReference: "src/selfhost/sentry.ts", }, { name: "SENTRY_TRACES_SAMPLE_RATE", - firstReference: "src/selfhost/sentry.ts:195", + firstReference: "src/selfhost/sentry.ts", }, { name: "SETUP_OUTPUT_PATH", - firstReference: "src/server.ts:838", + firstReference: "src/server.ts", }, { name: "SLACK_WEBHOOK_URL", - firstReference: "src/services/notify-discord.ts:173", + firstReference: "src/services/notify-discord.ts", }, ]; export const SELFHOST_ENV_REFERENCE_MARKDOWN = [ "| Name | First reference |", "| --- | --- |", - "| `AI_COMBINE` | `src/selfhost/ai.ts:1149` |", - "| `AI_DUAL_REVIEW` | `src/selfhost/ai.ts:1151` |", - "| `AI_EMBED_API_KEY` | `src/server.ts:443` |", - "| `AI_EMBED_BASE_URL` | `src/server.ts:440` |", - "| `AI_EMBED_MODEL` | `src/selfhost/ai.ts:1045` |", - "| `AI_ON_MERGE` | `src/selfhost/ai.ts:1149` |", - "| `AI_PROVIDER` | `src/selfhost/ai-config.ts:43` |", - "| `ANTHROPIC_AI_BASE_URL` | `src/selfhost/ai.ts:1049` |", - "| `ANTHROPIC_AI_MODEL` | `src/selfhost/ai.ts:109` |", - "| `ANTHROPIC_API_KEY` | `src/selfhost/ai.ts:1048` |", - "| `BACKUP_ACKNOWLEDGED` | `src/server.ts:381` |", - "| `BROWSER_WS_ENDPOINT` | `src/selfhost/stubs/puppeteer.ts:11` |", - "| `CLAUDE_AI_EFFORT` | `src/selfhost/ai.ts:160` |", - "| `CLAUDE_AI_MODEL` | `src/selfhost/ai.ts:101` |", - "| `CLAUDE_AI_TIMEOUT_MS` | `src/selfhost/ai.ts:160` |", - "| `CODEX_AI_EFFORT` | `src/selfhost/ai.ts:164` |", - "| `CODEX_AI_FIRST_OUTPUT_TIMEOUT_MS` | `src/selfhost/ai.ts:184` |", - "| `CODEX_AI_MODEL` | `src/selfhost/ai.ts:105` |", - "| `CODEX_AI_TIMEOUT_MS` | `src/selfhost/ai.ts:164` |", - "| `CODEX_HOME` | `src/selfhost/ai.ts:353` |", - "| `CRON_INTERVAL_MS` | `src/server.ts:922` |", - "| `DATABASE_PATH` | `src/server.ts:251` |", - "| `DATABASE_URL` | `src/selfhost/preflight.ts:201` |", - "| `DISCORD_REPO_WEBHOOKS` | `src/services/notify-discord.ts:41` |", - "| `DISCORD_WEBHOOK_URL` | `src/services/notify-discord.ts:78` |", - "| `FOREGROUND_LIVENESS_CHECK_INTERVAL_MS` | `src/selfhost/foreground-liveness.ts:52` |", - "| `FOREGROUND_LIVENESS_ENABLED` | `src/selfhost/foreground-liveness.ts:41` |", - "| `FOREGROUND_LIVENESS_MAX_DEFER_MS` | `src/selfhost/foreground-liveness.ts:51` |", - "| `FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP` | `src/selfhost/foreground-liveness.ts:53` |", - "| `GITHUB_APP_ID` | `src/selfhost/orb-collector.ts:59` |", - "| `GITHUB_APP_PRIVATE_KEY` | `src/selfhost/orb-collector.ts:166` |", - "| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts:512` |", - "| `GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS` | `src/selfhost/installation-concurrency-admission.ts:47` |", - "| `GITHUB_INSTALLATION_CONCURRENCY_ENABLED` | `src/selfhost/installation-concurrency-admission.ts:34` |", - "| `GITHUB_INSTALLATION_CONCURRENCY_LIMIT` | `src/selfhost/installation-concurrency-admission.ts:43` |", - "| `GITTENSORY_REPO_CONFIG_DIR` | `src/server.ts:290` |", - "| `GITTENSORY_VERSION` | `src/selfhost/otel.ts:62` |", - "| `HOME` | `src/selfhost/ai.ts:353` |", - "| `MAINTENANCE_ADMISSION_DEFER_MS` | `src/selfhost/maintenance-admission.ts:171` |", - "| `MAINTENANCE_ADMISSION_DRAIN_AGE_MS` | `src/selfhost/maintenance-admission.ts:145` |", - "| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts:126` |", - "| `MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING` | `src/selfhost/maintenance-admission.ts:167` |", - "| `MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS` | `src/selfhost/maintenance-admission.ts:141` |", - "| `MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS` | `src/selfhost/maintenance-admission.ts:155` |", - "| `MAINTENANCE_ADMISSION_MAX_LIVE_PENDING` | `src/selfhost/maintenance-admission.ts:151` |", - "| `MAINTENANCE_ADMISSION_MAX_PENDING` | `src/selfhost/maintenance-admission.ts:159` |", - "| `MIGRATIONS_DIR` | `src/server.ts:395` |", - "| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.mjs:8` |", - "| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.mjs:6` |", - "| `OLLAMA_AI_API_KEY` | `src/selfhost/ai.ts:1042` |", - "| `OLLAMA_AI_BASE_URL` | `src/selfhost/ai.ts:1038` |", - "| `OLLAMA_AI_MODEL` | `src/selfhost/ai.ts:113` |", - "| `OPENAI_AI_BASE_URL` | `src/selfhost/ai.ts:1040` |", - "| `OPENAI_AI_MODEL` | `src/selfhost/ai.ts:114` |", - "| `OPENAI_API_KEY` | `src/selfhost/ai.ts:1042` |", - "| `OPENAI_COMPATIBLE_AI_API_KEY` | `src/selfhost/ai.ts:1042` |", - "| `OPENAI_COMPATIBLE_AI_BASE_URL` | `src/selfhost/ai.ts:1041` |", - "| `OPENAI_COMPATIBLE_AI_MODEL` | `src/selfhost/ai.ts:115` |", - "| `ORB_AIR_GAP` | `src/selfhost/orb-collector.ts:161` |", - "| `ORB_ANONYMIZE` | `src/selfhost/orb-collector.ts:174` |", - "| `ORB_APP_ID` | `src/selfhost/orb-collector.ts:59` |", - "| `ORB_BROKER_URL` | `src/server.ts:977` |", - "| `ORB_COLLECTOR_TOKEN` | `src/selfhost/orb-collector.ts:205` |", - "| `ORB_COLLECTOR_URL` | `src/selfhost/orb-collector.ts:172` |", - "| `ORB_ENROLLMENT_SECRET` | `src/selfhost/orb-collector.ts:165` |", - "| `ORB_RELAY_MODE` | `src/server.ts:965` |", - "| `OTEL_EXPORTER_OTLP_ENDPOINT` | `src/selfhost/otel.ts:47` |", - "| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `src/selfhost/otel.ts:45` |", - "| `OTEL_SERVICE_ENVIRONMENT` | `src/selfhost/otel.ts:60` |", - "| `OTEL_SERVICE_NAME` | `src/selfhost/otel.ts:59` |", - "| `OTEL_TRACES_EXPORTER` | `src/selfhost/otel.ts:40` |", - "| `OTEL_TRACES_SAMPLER` | `src/selfhost/otel.ts:74` |", - "| `OTEL_TRACES_SAMPLER_ARG` | `src/selfhost/otel.ts:76` |", - "| `PGPOOL_MAX` | `src/selfhost/queue-common.ts:713` |", - "| `PGVECTOR_ENABLED` | `src/server.ts:231` |", - "| `PORT` | `src/server.ts:721` |", - "| `PUBLIC_API_ORIGIN` | `src/selfhost/preflight.ts:192` |", - "| `QDRANT_API_KEY` | `src/selfhost/qdrant-vectorize.ts:50` |", - "| `QDRANT_DIM` | `src/selfhost/qdrant-vectorize.ts:71` |", - "| `QDRANT_URL` | `src/server.ts:531` |", - "| `QUEUE_BACKGROUND_CONCURRENCY` | `src/selfhost/queue-common.ts:130` |", - "| `QUEUE_CONCURRENCY` | `src/selfhost/pg-queue.ts:286` |", - "| `QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS` | `src/selfhost/queue-common.ts:721` |", - "| `QUEUE_STARTUP_JITTER_MIN_JOBS` | `src/selfhost/queue-common.ts:702` |", - "| `REDIS_URL` | `src/selfhost/preflight.ts:144` |", - "| `REVIEW_AUDIT_DIR` | `src/server.ts:576` |", - "| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.mjs:13` |", - "| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.mjs:5` |", - "| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts:186` |", - "| `SENTRY_DSN` | `src/selfhost/sentry.ts:389` |", - "| `SENTRY_ENVIRONMENT` | `src/selfhost/otel.ts:60` |", - "| `SENTRY_RELEASE` | `src/selfhost/otel.ts:62` |", - "| `SENTRY_SERVER_NAME` | `src/selfhost/sentry.ts:407` |", - "| `SENTRY_TRACES_SAMPLE_RATE` | `src/selfhost/sentry.ts:195` |", - "| `SETUP_OUTPUT_PATH` | `src/server.ts:838` |", - "| `SLACK_WEBHOOK_URL` | `src/services/notify-discord.ts:173` |", + "| `AI_COMBINE` | `src/selfhost/ai.ts` |", + "| `AI_DUAL_REVIEW` | `src/selfhost/ai.ts` |", + "| `AI_EMBED_API_KEY` | `src/server.ts` |", + "| `AI_EMBED_BASE_URL` | `src/server.ts` |", + "| `AI_EMBED_MODEL` | `src/selfhost/ai.ts` |", + "| `AI_ON_MERGE` | `src/selfhost/ai.ts` |", + "| `AI_PROVIDER` | `src/selfhost/ai-config.ts` |", + "| `ANTHROPIC_AI_BASE_URL` | `src/selfhost/ai.ts` |", + "| `ANTHROPIC_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `ANTHROPIC_API_KEY` | `src/selfhost/ai.ts` |", + "| `BACKUP_ACKNOWLEDGED` | `src/server.ts` |", + "| `BROWSER_WS_ENDPOINT` | `src/selfhost/stubs/puppeteer.ts` |", + "| `CLAUDE_AI_EFFORT` | `src/selfhost/ai.ts` |", + "| `CLAUDE_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `CLAUDE_AI_TIMEOUT_MS` | `src/selfhost/ai.ts` |", + "| `CODEX_AI_EFFORT` | `src/selfhost/ai.ts` |", + "| `CODEX_AI_FIRST_OUTPUT_TIMEOUT_MS` | `src/selfhost/ai.ts` |", + "| `CODEX_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `CODEX_AI_TIMEOUT_MS` | `src/selfhost/ai.ts` |", + "| `CODEX_HOME` | `src/selfhost/ai.ts` |", + "| `CRON_INTERVAL_MS` | `src/server.ts` |", + "| `DATABASE_PATH` | `src/server.ts` |", + "| `DATABASE_URL` | `src/selfhost/preflight.ts` |", + "| `DISCORD_REPO_WEBHOOKS` | `src/services/notify-discord.ts` |", + "| `DISCORD_WEBHOOK_URL` | `src/services/notify-discord.ts` |", + "| `FOREGROUND_LIVENESS_CHECK_INTERVAL_MS` | `src/selfhost/foreground-liveness.ts` |", + "| `FOREGROUND_LIVENESS_ENABLED` | `src/selfhost/foreground-liveness.ts` |", + "| `FOREGROUND_LIVENESS_MAX_DEFER_MS` | `src/selfhost/foreground-liveness.ts` |", + "| `FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP` | `src/selfhost/foreground-liveness.ts` |", + "| `GITHUB_APP_ID` | `src/selfhost/orb-collector.ts` |", + "| `GITHUB_APP_PRIVATE_KEY` | `src/selfhost/orb-collector.ts` |", + "| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts` |", + "| `GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS` | `src/selfhost/installation-concurrency-admission.ts` |", + "| `GITHUB_INSTALLATION_CONCURRENCY_ENABLED` | `src/selfhost/installation-concurrency-admission.ts` |", + "| `GITHUB_INSTALLATION_CONCURRENCY_LIMIT` | `src/selfhost/installation-concurrency-admission.ts` |", + "| `GITTENSORY_REPO_CONFIG_DIR` | `src/server.ts` |", + "| `GITTENSORY_VERSION` | `src/selfhost/otel.ts` |", + "| `HOME` | `src/selfhost/ai.ts` |", + "| `MAINTENANCE_ADMISSION_DEFER_MS` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_DRAIN_AGE_MS` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_MAX_LIVE_PENDING` | `src/selfhost/maintenance-admission.ts` |", + "| `MAINTENANCE_ADMISSION_MAX_PENDING` | `src/selfhost/maintenance-admission.ts` |", + "| `MIGRATIONS_DIR` | `src/server.ts` |", + "| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.mjs` |", + "| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.mjs` |", + "| `OLLAMA_AI_API_KEY` | `src/selfhost/ai.ts` |", + "| `OLLAMA_AI_BASE_URL` | `src/selfhost/ai.ts` |", + "| `OLLAMA_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `OPENAI_AI_BASE_URL` | `src/selfhost/ai.ts` |", + "| `OPENAI_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `OPENAI_API_KEY` | `src/selfhost/ai.ts` |", + "| `OPENAI_COMPATIBLE_AI_API_KEY` | `src/selfhost/ai.ts` |", + "| `OPENAI_COMPATIBLE_AI_BASE_URL` | `src/selfhost/ai.ts` |", + "| `OPENAI_COMPATIBLE_AI_MODEL` | `src/selfhost/ai.ts` |", + "| `ORB_AIR_GAP` | `src/selfhost/orb-collector.ts` |", + "| `ORB_ANONYMIZE` | `src/selfhost/orb-collector.ts` |", + "| `ORB_APP_ID` | `src/selfhost/orb-collector.ts` |", + "| `ORB_BROKER_URL` | `src/server.ts` |", + "| `ORB_COLLECTOR_TOKEN` | `src/selfhost/orb-collector.ts` |", + "| `ORB_COLLECTOR_URL` | `src/selfhost/orb-collector.ts` |", + "| `ORB_ENROLLMENT_SECRET` | `src/selfhost/orb-collector.ts` |", + "| `ORB_RELAY_MODE` | `src/server.ts` |", + "| `OTEL_EXPORTER_OTLP_ENDPOINT` | `src/selfhost/otel.ts` |", + "| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `src/selfhost/otel.ts` |", + "| `OTEL_SERVICE_ENVIRONMENT` | `src/selfhost/otel.ts` |", + "| `OTEL_SERVICE_NAME` | `src/selfhost/otel.ts` |", + "| `OTEL_TRACES_EXPORTER` | `src/selfhost/otel.ts` |", + "| `OTEL_TRACES_SAMPLER` | `src/selfhost/otel.ts` |", + "| `OTEL_TRACES_SAMPLER_ARG` | `src/selfhost/otel.ts` |", + "| `PGPOOL_MAX` | `src/selfhost/queue-common.ts` |", + "| `PGVECTOR_ENABLED` | `src/server.ts` |", + "| `PORT` | `src/server.ts` |", + "| `PUBLIC_API_ORIGIN` | `src/selfhost/preflight.ts` |", + "| `QDRANT_API_KEY` | `src/selfhost/qdrant-vectorize.ts` |", + "| `QDRANT_DIM` | `src/selfhost/qdrant-vectorize.ts` |", + "| `QDRANT_URL` | `src/server.ts` |", + "| `QUEUE_BACKGROUND_CONCURRENCY` | `src/selfhost/queue-common.ts` |", + "| `QUEUE_CONCURRENCY` | `src/selfhost/pg-queue.ts` |", + "| `QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS` | `src/selfhost/queue-common.ts` |", + "| `QUEUE_STARTUP_JITTER_MIN_JOBS` | `src/selfhost/queue-common.ts` |", + "| `REDIS_URL` | `src/selfhost/preflight.ts` |", + "| `REVIEW_AUDIT_DIR` | `src/server.ts` |", + "| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.mjs` |", + "| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.mjs` |", + "| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts` |", + "| `SENTRY_DSN` | `src/selfhost/sentry.ts` |", + "| `SENTRY_ENVIRONMENT` | `src/selfhost/otel.ts` |", + "| `SENTRY_RELEASE` | `src/selfhost/otel.ts` |", + "| `SENTRY_SERVER_NAME` | `src/selfhost/sentry.ts` |", + "| `SENTRY_TRACES_SAMPLE_RATE` | `src/selfhost/sentry.ts` |", + "| `SETUP_OUTPUT_PATH` | `src/server.ts` |", + "| `SLACK_WEBHOOK_URL` | `src/services/notify-discord.ts` |", ].join("\n"); diff --git a/scripts/gen-selfhost-env-reference.mjs b/scripts/gen-selfhost-env-reference.mjs index 76c8406007..7a3bc773d1 100644 --- a/scripts/gen-selfhost-env-reference.mjs +++ b/scripts/gen-selfhost-env-reference.mjs @@ -37,29 +37,33 @@ export function collectSelfHostEnvVars({ for (const file of sourceFiles(rootDir, sourceRoots)) { const abs = resolve(rootDir, file); for (const read of collectEnvReads(readFileSync(abs, "utf8"), file)) { - if (!rows.has(read.name)) rows.set(read.name, { name: read.name, firstReference: `${file}:${read.line}` }); + if (!rows.has(read.name)) rows.set(read.name, { name: read.name, firstReference: file }); } } return [...rows.values()].sort((a, b) => a.name.localeCompare(b.name)); } +// Deliberately file-only, not `file:line` (#env-reference-churn) -- a line number makes the generated output +// change whenever ANYTHING above an existing read shifts, so two unrelated PRs touching the same source file +// produce two different regenerated rows and collide on rebase. The file path only changes when a read is +// actually added/removed/moved to a different file, which is the only case that should ever require +// regenerating this doc. function collectEnvReads(source, fileName) { const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName)); const reads = []; - const addRead = (name, node) => { + const addRead = (name) => { if (!ENV_NAME_RE.test(name) || INJECTED_BINDING_NAMES.has(name)) return; - const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); - reads.push({ name, line: line + 1 }); + reads.push({ name }); }; const visit = (node) => { if (ts.isPropertyAccessExpression(node) && isEnvContainer(node.expression)) { - addRead(node.name.text, node.name); + addRead(node.name.text); } else if ( ts.isElementAccessExpression(node) && isEnvContainer(node.expression) && ts.isStringLiteralLike(node.argumentExpression) ) { - addRead(node.argumentExpression.text, node.argumentExpression); + addRead(node.argumentExpression.text); } else if ( ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && @@ -68,12 +72,12 @@ function collectEnvReads(source, fileName) { ) { for (const element of node.name.elements) { const name = bindingElementName(element); - if (name) addRead(name, element.propertyName ?? element.name); + if (name) addRead(name); } } else if (ts.isCallExpression(node) && isStaticEnvHelperCall(node)) { - addRead(node.arguments[1].text, node.arguments[1]); + addRead(node.arguments[1].text); } else if (ts.isCallExpression(node) && isProcessEnvNameHelperCall(node)) { - addRead(node.arguments[0].text, node.arguments[0]); + addRead(node.arguments[0].text); } ts.forEachChild(node, visit); }; diff --git a/test/unit/selfhost-env-reference-script.test.ts b/test/unit/selfhost-env-reference-script.test.ts index 1e57cf62ab..1266903f73 100644 --- a/test/unit/selfhost-env-reference-script.test.ts +++ b/test/unit/selfhost-env-reference-script.test.ts @@ -62,26 +62,44 @@ function fixtureRoot(): string { describe("gen-selfhost-env-reference (#2081)", () => { it("extracts static env reads and keeps the first source reference", () => { expect(collectSelfHostEnvVars({ rootDir: fixtureRoot() })).toEqual([ - { name: "ALIASED_ENV", firstReference: "src/selfhost/a.ts:5" }, - { name: "BRACKET_ONLY", firstReference: "src/selfhost/a.ts:4" }, - { name: "CASTED_ONLY", firstReference: "src/services/notify-discord.ts:3" }, - { name: "CTX_ONLY", firstReference: "src/selfhost/a.ts:10" }, - { name: "DEFAULTED_ENV", firstReference: "src/selfhost/a.ts:5" }, - { name: "DESTRUCTURED", firstReference: "src/selfhost/a.ts:5" }, - { name: "FIRST", firstReference: "src/selfhost/a.ts:3" }, - { name: "HELPER_ONLY", firstReference: "src/selfhost/a.ts:6" }, - { name: "NESTED_ONLY", firstReference: "src/selfhost/nested/b.ts:2" }, - { name: "OBJECT_ALIASED", firstReference: "src/selfhost/a.ts:9" }, - { name: "OBJECT_BRACKET", firstReference: "src/selfhost/a.ts:8" }, - { name: "OBJECT_DESTRUCTURED", firstReference: "src/selfhost/a.ts:9" }, - { name: "PARSED_INT_ONLY", firstReference: "src/services/notify-discord.ts:4" }, - { name: "SECOND", firstReference: "src/selfhost/a.ts:2" }, - { name: "SERVER_ONLY", firstReference: "src/server.ts:1" }, - { name: "SERVICE_HELPER_ONLY", firstReference: "src/services/notify-discord.ts:2" }, - { name: "SERVICE_ONLY", firstReference: "src/services/notify-discord.ts:1" }, + { name: "ALIASED_ENV", firstReference: "src/selfhost/a.ts" }, + { name: "BRACKET_ONLY", firstReference: "src/selfhost/a.ts" }, + { name: "CASTED_ONLY", firstReference: "src/services/notify-discord.ts" }, + { name: "CTX_ONLY", firstReference: "src/selfhost/a.ts" }, + { name: "DEFAULTED_ENV", firstReference: "src/selfhost/a.ts" }, + { name: "DESTRUCTURED", firstReference: "src/selfhost/a.ts" }, + { name: "FIRST", firstReference: "src/selfhost/a.ts" }, + { name: "HELPER_ONLY", firstReference: "src/selfhost/a.ts" }, + { name: "NESTED_ONLY", firstReference: "src/selfhost/nested/b.ts" }, + { name: "OBJECT_ALIASED", firstReference: "src/selfhost/a.ts" }, + { name: "OBJECT_BRACKET", firstReference: "src/selfhost/a.ts" }, + { name: "OBJECT_DESTRUCTURED", firstReference: "src/selfhost/a.ts" }, + { name: "PARSED_INT_ONLY", firstReference: "src/services/notify-discord.ts" }, + { name: "SECOND", firstReference: "src/selfhost/a.ts" }, + { name: "SERVER_ONLY", firstReference: "src/server.ts" }, + { name: "SERVICE_HELPER_ONLY", firstReference: "src/services/notify-discord.ts" }, + { name: "SERVICE_ONLY", firstReference: "src/services/notify-discord.ts" }, ]); }); + it("REGRESSION (#env-reference-churn): firstReference is immune to line shifts elsewhere in the file", () => { + const root = mkdtempSync(join(tmpdir(), "gt-env-reference-lineshift-")); + mkdirSync(join(root, "src", "selfhost"), { recursive: true }); + const filePath = join(root, "src", "selfhost", "a.ts"); + const read = "const value = process.env.STABLE_VAR;\n"; + + writeFileSync(filePath, read); + const before = collectSelfHostEnvVars({ rootDir: root }); + expect(before).toEqual([{ name: "STABLE_VAR", firstReference: "src/selfhost/a.ts" }]); + + // Ten unrelated lines added ABOVE the same read -- a real PR touching this file for an unrelated reason + // would shift STABLE_VAR from line 1 to line 11 under the old file:line format, changing the generated + // output and colliding with any other PR that regenerated it from a different line count. + writeFileSync(filePath, "// unrelated change\n".repeat(10) + read); + const after = collectSelfHostEnvVars({ rootDir: root }); + expect(after).toEqual(before); + }); + it("scans configured JavaScript roots and rejects file-shaped directories", () => { const root = fixtureRoot(); @@ -90,7 +108,7 @@ describe("gen-selfhost-env-reference (#2081)", () => { rootDir: root, sourceRoots: ["scripts/selfhost-smoke.mjs"], }), - ).toEqual([{ name: "SCRIPT_ONLY", firstReference: "scripts/selfhost-smoke.mjs:1" }]); + ).toEqual([{ name: "SCRIPT_ONLY", firstReference: "scripts/selfhost-smoke.mjs" }]); mkdirSync(join(root, "src", "selfhost", "bad.ts")); expect(() => @@ -104,15 +122,15 @@ describe("gen-selfhost-env-reference (#2081)", () => { it("renders a deterministic Markdown table with names and references only", () => { expect( renderSelfHostEnvReferenceMarkdown([ - { name: "FIRST", firstReference: "src/selfhost/a.ts:3" }, - { name: "SECOND", firstReference: "src/selfhost/a.ts:2" }, + { name: "FIRST", firstReference: "src/selfhost/a.ts" }, + { name: "SECOND", firstReference: "src/server.ts" }, ]), ).toBe( [ "| Name | First reference |", "| --- | --- |", - "| `FIRST` | `src/selfhost/a.ts:3` |", - "| `SECOND` | `src/selfhost/a.ts:2` |", + "| `FIRST` | `src/selfhost/a.ts` |", + "| `SECOND` | `src/server.ts` |", ].join("\n"), ); }); @@ -127,7 +145,7 @@ describe("gen-selfhost-env-reference (#2081)", () => { expect(existsSync(outputAbs)).toBe(true); const generated = readFileSync(outputAbs, "utf8"); expect(generated).toContain("SELFHOST_ENV_REFERENCE_MARKDOWN"); - expect(generated).toContain("src/selfhost/a.ts:3"); + expect(generated).toContain("src/selfhost/a.ts"); expect(writeSelfHostEnvReference({ rootDir: root, outputPath, check: true }).changed).toBe(false);