diff --git a/apps/loopover-ui/content/docs/ams-config-precedence.mdx b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
index b86de459f5..ebbe74655f 100644
--- a/apps/loopover-ui/content/docs/ams-config-precedence.mdx
+++ b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
@@ -182,5 +182,5 @@ goal-spec override.
## See also
- [`packages/loopover-miner/docs/miner-goal-spec.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/miner-goal-spec.md) — goal-spec field reference
-- [`packages/loopover-miner/docs/env-reference.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/env-reference.md) — generated operator env list
+- [AMS environment variable reference](/docs/ams-env-reference) — generated operator env list
- ORB's `.loopover.yml` precedence (`yml > DB > defaults`) in [Self-hosting configuration](/docs/self-hosting-configuration) — analogous documentation style, different runtime
diff --git a/apps/loopover-ui/content/docs/ams-env-reference.mdx b/apps/loopover-ui/content/docs/ams-env-reference.mdx
new file mode 100644
index 0000000000..90addd381d
--- /dev/null
+++ b/apps/loopover-ui/content/docs/ams-env-reference.mdx
@@ -0,0 +1,24 @@
+---
+title: AMS environment variable reference
+description: Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.
+---
+
+import { AMS_ENV_REFERENCE_MARKDOWN } from "@/lib/ams-env-reference";
+
+AMS's environment-variable reference: every `LOOPOVER_MINER_*` / `MINER_*` var the miner reads,
+where it's first referenced in code, and its default. This is the AMS-side counterpart to
+[Self-hosting configuration](/docs/self-hosting-configuration)'s generated env reference — see
+the [AMS deployment guide](/docs/ams-deployment) and [AMS config
+precedence](/docs/ams-config-precedence) for how these variables fit into the broader
+laptop/fleet setup and precedence model.
+
+
+ This table is generated from `env.NAME` reads (and the `resolveLocalStoreDbPath` /
+ `LOOPOVER_MINER_*_DB` per-store override pattern) under
+ `packages/loopover-miner/{bin,lib,scripts}`. It intentionally includes names, first source
+ references, and defaults only, never example values — and it's checked for drift in CI
+ (`npm run miner:env-reference:check`), so a new `LOOPOVER_MINER_*`/`MINER_*` read that isn't
+ reflected here fails the build until this reference is regenerated.
+
+
+
diff --git a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
index bf4cbea18f..0a78b2641a 100644
--- a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
+++ b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
@@ -275,6 +275,6 @@ store filename.
- [AMS deployment guide](/docs/ams-deployment) — laptop vs fleet, volumes, systemd, scaling rules
- [`packages/loopover-miner/README.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/README.md#local-storage) — store inventory
-- [`packages/loopover-miner/docs/env-reference.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/env-reference.md) — per-store path overrides
+- [AMS environment variable reference](/docs/ams-env-reference) — per-store path overrides
- [`packages/loopover-miner/docs/coding-agent-driver.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/coding-agent-driver.md) — attempt log semantics
- [`packages/loopover-miner/docs/discovery-plane-operator-guide.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/discovery-plane-operator-guide.md) — optional hosted plane (distinct from local ledger ops)
diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx
index 3c5683df22..a8b2149855 100644
--- a/apps/loopover-ui/src/components/site/docs-nav.tsx
+++ b/apps/loopover-ui/src/components/site/docs-nav.tsx
@@ -81,6 +81,7 @@ export const docsNav: DocsGroup[] = [
{ to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
{ to: "/docs/ams-sizing", label: "Resource sizing" },
{ to: "/docs/ams-config-precedence", label: "Config precedence" },
+ { to: "/docs/ams-env-reference", label: "Env var reference" },
],
},
],
diff --git a/apps/loopover-ui/src/lib/ams-env-reference.ts b/apps/loopover-ui/src/lib/ams-env-reference.ts
new file mode 100644
index 0000000000..3029566d56
--- /dev/null
+++ b/apps/loopover-ui/src/lib/ams-env-reference.ts
@@ -0,0 +1,199 @@
+// Generated by scripts/generate-env-reference.mjs (npm run miner:env-reference). Do not edit manually.
+export type MinerEnvReferenceRow = {
+ name: string;
+ firstReference: string;
+ defaultValue: string | null;
+};
+
+export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
+ {
+ name: "LOOPOVER_MINER_AMS_COLLECTOR_TOKEN",
+ firstReference: "lib/orb-export.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_AMS_COLLECTOR_URL",
+ firstReference: "lib/orb-export.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID",
+ firstReference: "lib/oauth-device-flow.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_AMS_POLICY_PATH",
+ firstReference: "lib/ams-policy.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_ATTEMPT_LOG_DB",
+ firstReference: "lib/attempt-log.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_CLAIM_LEDGER_DB",
+ firstReference: "lib/claim-ledger.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_CONFIG_DIR",
+ firstReference: "lib/deny-hook-synthesis.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB",
+ firstReference: "lib/deny-hook-synthesis.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_EVENT_LEDGER_DB",
+ firstReference: "lib/event-ledger.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_GOVERNOR_LEDGER_DB",
+ firstReference: "lib/governor-ledger.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_GOVERNOR_STATE_DB",
+ firstReference: "lib/governor-state.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_KILL_SWITCH",
+ firstReference: "lib/config-precedence.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_LOG_LEVEL",
+ firstReference: "lib/logger.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_NO_UPDATE_CHECK",
+ firstReference: "lib/update-check.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_ORB_EXPORT_DB",
+ firstReference: "lib/orb-export.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_PLAN_STORE_DB",
+ firstReference: "lib/plan-store.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_POLICY_DOC_CACHE_DB",
+ firstReference: "lib/policy-doc-cache.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB",
+ firstReference: "lib/policy-verdict-cache.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_PORTFOLIO_QUEUE_DB",
+ firstReference: "lib/portfolio-queue.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_PREDICTION_LEDGER_DB",
+ firstReference: "lib/prediction-ledger.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_RANKED_CANDIDATES_DB",
+ firstReference: "lib/ranked-candidates.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_REPLAY_SNAPSHOT_DB",
+ firstReference: "lib/replay-snapshot.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_REPO_CLONE_DIR",
+ firstReference: "lib/repo-clone.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_RUN_STATE_DB",
+ firstReference: "lib/run-state.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_SENTRY_DSN",
+ firstReference: "lib/sentry.js",
+ defaultValue: null,
+ },
+ {
+ name: "LOOPOVER_MINER_SENTRY_ENVIRONMENT",
+ firstReference: "lib/sentry.js",
+ defaultValue: "production",
+ },
+ {
+ name: "LOOPOVER_MINER_VERSION",
+ firstReference: "lib/version.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB",
+ firstReference: "lib/worktree-allocator.js",
+ defaultValue: "",
+ },
+ {
+ name: "LOOPOVER_MINER_WORKTREE_DIR",
+ firstReference: "lib/worktree-allocator.js",
+ defaultValue: "",
+ },
+ {
+ name: "MINER_CODING_AGENT_PROVIDER",
+ firstReference: "lib/laptop-init.js",
+ defaultValue: null,
+ },
+];
+
+export const AMS_ENV_REFERENCE_MARKDOWN = [
+ "# loopover-miner environment variable reference",
+ "",
+ "Generated by `npm run miner:env-reference`. Do not edit manually.",
+ "",
+ "| Name | First reference | Default |",
+ "| --- | --- | --- |",
+ '| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.js` | `""` |',
+ '| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.js` | `""` |',
+ '| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.js` | `""` |',
+ "| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.js` | (none) |",
+ "| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.js` | (none) |",
+ "| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.js` | (none) |",
+ '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.js` | `""` |',
+ '| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.js` | `""` |',
+ "| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.js` | (none) |",
+ '| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | `""` |',
+ "| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) |",
+ '| `LOOPOVER_MINER_KILL_SWITCH` | `lib/config-precedence.js` | `""` |',
+ '| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.js` | `""` |',
+ '| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` |',
+ '| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` |',
+ '| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | `""` |',
+ "| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.js` | (none) |",
+ "| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.js` | (none) |",
+ "| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.js` | (none) |",
+ '| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.js` | `""` |',
+ "| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.js` | (none) |",
+ "| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.js` | (none) |",
+ '| `LOOPOVER_MINER_REPO_CLONE_DIR` | `lib/repo-clone.js` | `""` |',
+ "| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.js` | (none) |",
+ "| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.js` | (none) |",
+ '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `"production"` |',
+ '| `LOOPOVER_MINER_VERSION` | `lib/version.js` | `""` |',
+ '| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.js` | `""` |',
+ '| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.js` | `""` |',
+ "| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.js` | (none) |",
+ "",
+].join("\n");
diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts
index bac1be6423..f8fc03779f 100644
--- a/apps/loopover-ui/src/routeTree.gen.ts
+++ b/apps/loopover-ui/src/routeTree.gen.ts
@@ -61,6 +61,7 @@ import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.a
import { Route as DocsAmsSizingRouteImport } from './routes/docs.ams-sizing'
import { Route as DocsAmsOperationsRunbookRouteImport } from './routes/docs.ams-operations-runbook'
import { Route as DocsAmsObservabilityRouteImport } from './routes/docs.ams-observability'
+import { Route as DocsAmsEnvReferenceRouteImport } from './routes/docs.ams-env-reference'
import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment'
import { Route as DocsAmsConfigPrecedenceRouteImport } from './routes/docs.ams-config-precedence'
import { Route as DocsAiSummariesRouteImport } from './routes/docs.ai-summaries'
@@ -356,6 +357,11 @@ const DocsAmsObservabilityRoute = DocsAmsObservabilityRouteImport.update({
path: '/ams-observability',
getParentRoute: () => DocsRoute,
} as any)
+const DocsAmsEnvReferenceRoute = DocsAmsEnvReferenceRouteImport.update({
+ id: '/ams-env-reference',
+ path: '/ams-env-reference',
+ getParentRoute: () => DocsRoute,
+} as any)
const DocsAmsDeploymentRoute = DocsAmsDeploymentRouteImport.update({
id: '/ams-deployment',
path: '/ams-deployment',
@@ -475,6 +481,7 @@ export interface FileRoutesByFullPath {
'/docs/ai-summaries': typeof DocsAiSummariesRoute
'/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
+ '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
'/docs/ams-sizing': typeof DocsAmsSizingRoute
@@ -544,6 +551,7 @@ export interface FileRoutesByTo {
'/docs/ai-summaries': typeof DocsAiSummariesRoute
'/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
+ '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
'/docs/ams-sizing': typeof DocsAmsSizingRoute
@@ -617,6 +625,7 @@ export interface FileRoutesById {
'/docs/ai-summaries': typeof DocsAiSummariesRoute
'/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
+ '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
'/docs/ams-sizing': typeof DocsAmsSizingRoute
@@ -691,6 +700,7 @@ export interface FileRouteTypes {
| '/docs/ai-summaries'
| '/docs/ams-config-precedence'
| '/docs/ams-deployment'
+ | '/docs/ams-env-reference'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
| '/docs/ams-sizing'
@@ -760,6 +770,7 @@ export interface FileRouteTypes {
| '/docs/ai-summaries'
| '/docs/ams-config-precedence'
| '/docs/ams-deployment'
+ | '/docs/ams-env-reference'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
| '/docs/ams-sizing'
@@ -832,6 +843,7 @@ export interface FileRouteTypes {
| '/docs/ai-summaries'
| '/docs/ams-config-precedence'
| '/docs/ams-deployment'
+ | '/docs/ams-env-reference'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
| '/docs/ams-sizing'
@@ -1257,6 +1269,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsAmsObservabilityRouteImport
parentRoute: typeof DocsRoute
}
+ '/docs/ams-env-reference': {
+ id: '/docs/ams-env-reference'
+ path: '/ams-env-reference'
+ fullPath: '/docs/ams-env-reference'
+ preLoaderRoute: typeof DocsAmsEnvReferenceRouteImport
+ parentRoute: typeof DocsRoute
+ }
'/docs/ams-deployment': {
id: '/docs/ams-deployment'
path: '/ams-deployment'
@@ -1438,6 +1457,7 @@ interface DocsRouteChildren {
DocsAiSummariesRoute: typeof DocsAiSummariesRoute
DocsAmsConfigPrecedenceRoute: typeof DocsAmsConfigPrecedenceRoute
DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute
+ DocsAmsEnvReferenceRoute: typeof DocsAmsEnvReferenceRoute
DocsAmsObservabilityRoute: typeof DocsAmsObservabilityRoute
DocsAmsOperationsRunbookRoute: typeof DocsAmsOperationsRunbookRoute
DocsAmsSizingRoute: typeof DocsAmsSizingRoute
@@ -1484,6 +1504,7 @@ const DocsRouteChildren: DocsRouteChildren = {
DocsAiSummariesRoute: DocsAiSummariesRoute,
DocsAmsConfigPrecedenceRoute: DocsAmsConfigPrecedenceRoute,
DocsAmsDeploymentRoute: DocsAmsDeploymentRoute,
+ DocsAmsEnvReferenceRoute: DocsAmsEnvReferenceRoute,
DocsAmsObservabilityRoute: DocsAmsObservabilityRoute,
DocsAmsOperationsRunbookRoute: DocsAmsOperationsRunbookRoute,
DocsAmsSizingRoute: DocsAmsSizingRoute,
diff --git a/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx b/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx
new file mode 100644
index 0000000000..f78ef9d061
--- /dev/null
+++ b/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx
@@ -0,0 +1,53 @@
+import { createFileRoute, notFound } from "@tanstack/react-router";
+import { Suspense } from "react";
+
+import { DocsPage } from "@/components/site/docs-page";
+import { docsClientLoader } from "@/lib/docs-client-loader";
+
+// Rendered from content/docs/ams-env-reference.mdx via fumadocs-mdx's browser entry
+// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock primitives --
+// not fumadocs-ui's bundled components. See docs-source.ts's comment for why the
+// loader below resolves only a plain, serializable path string. The .mdx content
+// itself imports AMS_ENV_REFERENCE_MARKDOWN from @/lib/ams-env-reference (generated by
+// packages/loopover-miner/scripts/generate-env-reference.mjs, drift-checked in CI),
+// the same pattern docs.self-hosting-configuration.tsx uses for
+// SELFHOST_ENV_REFERENCE_MARKDOWN.
+export const Route = createFileRoute("/docs/ams-env-reference")({
+ loader: async () => {
+ const { docsSource } = await import("@/lib/docs-source");
+ const page = docsSource.getPage(["ams-env-reference"]);
+ if (!page) throw notFound();
+ return { path: page.path, title: page.data.title, description: page.data.description };
+ },
+ head: () => ({
+ meta: [
+ { title: "AMS environment variable reference — LoopOver docs" },
+ {
+ name: "description",
+ content:
+ "Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.",
+ },
+ { property: "og:title", content: "AMS environment variable reference — LoopOver docs" },
+ {
+ property: "og:description",
+ content:
+ "Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.",
+ },
+ { property: "og:url", content: "/docs/ams-env-reference" },
+ ],
+ links: [{ rel: "canonical", href: "/docs/ams-env-reference" }],
+ }),
+ component: AmsEnvReference,
+});
+
+function AmsEnvReference() {
+ const { path, title, description } = Route.useLoaderData();
+ const Content = docsClientLoader.getComponent(path);
+ return (
+
+ Loading…
}>
+
+
+
+ );
+}
diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx
index a80c6012db..81a91e0bf1 100644
--- a/apps/loopover-ui/src/routes/docs.index.tsx
+++ b/apps/loopover-ui/src/routes/docs.index.tsx
@@ -79,6 +79,7 @@ const AUDIENCES: Audience[] = [
{ to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
{ to: "/docs/ams-sizing", label: "Resource sizing" },
{ to: "/docs/ams-config-precedence", label: "Config precedence" },
+ { to: "/docs/ams-env-reference", label: "Env var reference" },
{ to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" },
{ to: "/docs/maintainer-install-trust", label: "Install & trust guide" },
{ to: "/docs/github-app", label: "GitHub App configuration" },
diff --git a/packages/loopover-miner/scripts/generate-env-reference.d.mts b/packages/loopover-miner/scripts/generate-env-reference.d.mts
index 98318573ff..c2bef270f0 100644
--- a/packages/loopover-miner/scripts/generate-env-reference.d.mts
+++ b/packages/loopover-miner/scripts/generate-env-reference.d.mts
@@ -15,6 +15,7 @@ export type WriteMinerEnvReferenceOptions = MinerEnvReferenceOptions & {
};
export declare const DEFAULT_OUTPUT_PATH: string;
+export declare const DEFAULT_MODULE_OUTPUT_PATH: string;
export declare const DEFAULT_SOURCE_ROOTS: readonly string[];
export declare function isMinerEnvVar(name: string): boolean;
@@ -37,3 +38,13 @@ export declare function writeMinerEnvReference(
outputPath: string;
rows: MinerEnvReferenceRow[];
};
+
+export declare function renderMinerEnvReferenceModule(rows: MinerEnvReferenceRow[]): string;
+
+export declare function writeMinerEnvReferenceModule(
+ options?: WriteMinerEnvReferenceOptions,
+): {
+ changed: boolean;
+ outputPath: string;
+ rows: MinerEnvReferenceRow[];
+};
diff --git a/packages/loopover-miner/scripts/generate-env-reference.mjs b/packages/loopover-miner/scripts/generate-env-reference.mjs
index e719c4a83b..90188462a7 100644
--- a/packages/loopover-miner/scripts/generate-env-reference.mjs
+++ b/packages/loopover-miner/scripts/generate-env-reference.mjs
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
import { collectSelfHostEnvVars } from "../../../scripts/gen-selfhost-env-reference.mjs";
export const DEFAULT_OUTPUT_PATH = "packages/loopover-miner/docs/env-reference.md";
+export const DEFAULT_MODULE_OUTPUT_PATH = "apps/loopover-ui/src/lib/ams-env-reference.ts";
export const DEFAULT_SOURCE_ROOTS = [
"packages/loopover-miner/bin",
"packages/loopover-miner/lib",
@@ -100,6 +101,73 @@ export function writeMinerEnvReference({
return { changed, outputPath, rows };
}
+// Mirrors scripts/gen-selfhost-env-reference.mjs's renderSelfHostEnvReferenceModule -- same shape
+// (a typed rows array plus a pre-joined markdown string), so docs.ams-env-reference.tsx's content
+// can import AMS_ENV_REFERENCE_MARKDOWN and render it exactly the way
+// docs.self-hosting-configuration.tsx already renders SELFHOST_ENV_REFERENCE_MARKDOWN. The
+// row-level default value is AMS-specific (ORB's self-host reference has no equivalent column).
+// Project Prettier config defaults to double-quoted JS string literals (singleQuote: false), but
+// still switches a given string to single quotes when that needs fewer backslash-escapes -- which
+// every markdown line whose row has a quoted string defaultValue (rendered as `""`/`"production"`
+// in the table) hits, since JSON.stringify always double-quotes. Replicate that per-string choice
+// here so the generator's own output is already Prettier-clean, not dependent on a separate --fix.
+function quoteJsStringLiteral(value) {
+ const doubleQuoteCount = (value.match(/"/g) ?? []).length;
+ const singleQuoteCount = (value.match(/'/g) ?? []).length;
+ if (singleQuoteCount < doubleQuoteCount) {
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
+ return `'${escaped}'`;
+ }
+ return JSON.stringify(value);
+}
+
+export function renderMinerEnvReferenceModule(rows) {
+ const markdown = renderMinerEnvReferenceMarkdown(rows);
+ const rowLines = rows
+ .map(
+ (row) =>
+ ` {\n name: ${quoteJsStringLiteral(row.name)},\n firstReference: ${quoteJsStringLiteral(row.firstReference)},\n defaultValue: ${row.defaultValue === null ? "null" : quoteJsStringLiteral(row.defaultValue)},\n },`,
+ )
+ .join("\n");
+ const markdownLines = markdown
+ .split("\n")
+ .map((line) => ` ${quoteJsStringLiteral(line)},`)
+ .join("\n");
+ return `// Generated by scripts/generate-env-reference.mjs (npm run miner:env-reference). Do not edit manually.
+export type MinerEnvReferenceRow = {
+ name: string;
+ firstReference: string;
+ defaultValue: string | null;
+};
+
+export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [
+${rowLines}
+];
+
+export const AMS_ENV_REFERENCE_MARKDOWN = [
+${markdownLines}
+].join("\\n");
+`;
+}
+
+export function writeMinerEnvReferenceModule({
+ rootDir = process.cwd(),
+ outputPath = DEFAULT_MODULE_OUTPUT_PATH,
+ sourceRoots = DEFAULT_SOURCE_ROOTS,
+ check = false,
+} = {}) {
+ const rows = collectMinerEnvVars({ rootDir, sourceRoots });
+ const output = renderMinerEnvReferenceModule(rows);
+ const absOutput = resolve(rootDir, outputPath);
+ const current = existsSync(absOutput) ? readFileSync(absOutput, "utf8") : null;
+ const changed = current !== output;
+ if (!check && changed) {
+ mkdirSync(dirname(absOutput), { recursive: true });
+ writeFileSync(absOutput, output);
+ }
+ return { changed, outputPath, rows };
+}
+
function sourceFiles(rootDir, sourceRoots) {
const files = [];
for (const sourceRoot of sourceRoots) {
@@ -143,16 +211,21 @@ function toPosixPath(path) {
function main(argv) {
const check = argv.includes("--check");
- const result = writeMinerEnvReference({ check });
- if (check && result.changed) {
- process.stderr.write(
- `generate-env-reference: ${result.outputPath} is stale; run npm run miner:env-reference.\n`,
- );
+ const results = [writeMinerEnvReference({ check }), writeMinerEnvReferenceModule({ check })];
+ const stale = results.filter((result) => result.changed);
+ if (check && stale.length > 0) {
+ for (const result of stale) {
+ process.stderr.write(
+ `generate-env-reference: ${result.outputPath} is stale; run npm run miner:env-reference.\n`,
+ );
+ }
process.exit(1);
}
- process.stdout.write(
- `generate-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`,
- );
+ for (const result of results) {
+ process.stdout.write(
+ `generate-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`,
+ );
+ }
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
diff --git a/test/unit/miner-env-reference-script.test.ts b/test/unit/miner-env-reference-script.test.ts
index 24fc434b55..2ebc02e972 100644
--- a/test/unit/miner-env-reference-script.test.ts
+++ b/test/unit/miner-env-reference-script.test.ts
@@ -6,7 +6,9 @@ import {
collectMinerEnvVars,
isMinerEnvVar,
renderMinerEnvReferenceMarkdown,
+ renderMinerEnvReferenceModule,
writeMinerEnvReference,
+ writeMinerEnvReferenceModule,
} from "../../packages/loopover-miner/scripts/generate-env-reference.mjs";
function fixtureRoot(): string {
@@ -116,4 +118,67 @@ describe("generate-env-reference (#5179)", () => {
const second = readFileSync(join(root, outputPath), "utf8");
expect(second).toBe(first);
});
+
+ it("renders a generated TS module with a typed rows array and a joined markdown constant (#6028)", () => {
+ const output = renderMinerEnvReferenceModule([
+ { name: "LOOPOVER_MINER_CONFIG_DIR", firstReference: "lib/a.js", defaultValue: "" },
+ { name: "LOOPOVER_MINER_RUN_STATE_DB", firstReference: "lib/run-state.js", defaultValue: null },
+ ]);
+ expect(output).toContain("export const AMS_ENV_REFERENCE_ROWS");
+ expect(output).toContain("export const AMS_ENV_REFERENCE_MARKDOWN");
+ expect(output).toContain('name: "LOOPOVER_MINER_CONFIG_DIR"');
+ expect(output).toContain('defaultValue: ""');
+ expect(output).toContain("defaultValue: null");
+ expect(output).toContain("# loopover-miner environment variable reference");
+ });
+
+ it("REGRESSION: quotes a generated markdown line with single quotes when it contains embedded double quotes, matching Prettier's own singleQuote:false-but-fewer-escapes preference (#6028)", () => {
+ // The rendered markdown table cell for a quoted string default (`""`, `"production"`, ...)
+ // embeds literal " characters in the line -- JSON.stringify would always double-quote the JS
+ // string literal and escape those, which Prettier then reformats to single quotes to avoid the
+ // escaping. Generating single-quoted output directly keeps the generator's own output already
+ // Prettier-clean instead of depending on a separate --fix pass that a fresh `npm run
+ // miner:env-reference` would immediately undo.
+ const output = renderMinerEnvReferenceModule([
+ { name: "LOOPOVER_MINER_SENTRY_ENVIRONMENT", firstReference: "lib/sentry.js", defaultValue: "production" },
+ ]);
+ expect(output).toContain(
+ " '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `\"production\"` |',",
+ );
+ expect(output).not.toMatch(/"\| `LOOPOVER_MINER_SENTRY_ENVIRONMENT`/);
+ });
+
+ it("writes the generated TS module and reports stale output in check mode (#6028)", () => {
+ const root = fixtureRoot();
+ const outputPath = "apps/loopover-ui/src/lib/ams-env-reference.ts";
+ const outputAbs = join(root, outputPath);
+
+ const written = writeMinerEnvReferenceModule({ rootDir: root, outputPath });
+ expect(written.changed).toBe(true);
+ expect(existsSync(outputAbs)).toBe(true);
+ const generated = readFileSync(outputAbs, "utf8");
+ expect(generated).toContain("AMS_ENV_REFERENCE_MARKDOWN");
+ expect(generated).toContain("LOOPOVER_MINER_CONFIG_DIR");
+
+ expect(writeMinerEnvReferenceModule({ rootDir: root, outputPath, check: true }).changed).toBe(false);
+
+ writeFileSync(outputAbs, "stale\n");
+ const stale = writeMinerEnvReferenceModule({ rootDir: root, outputPath, check: true });
+ expect(stale.changed).toBe(true);
+ expect(readFileSync(outputAbs, "utf8")).toBe("stale\n");
+
+ const rewritten = writeMinerEnvReferenceModule({ rootDir: root, outputPath });
+ expect(rewritten.changed).toBe(true);
+ expect(readFileSync(outputAbs, "utf8")).toBe(generated);
+ });
+
+ it("REGRESSION: TS module output is byte-for-byte deterministic across two runs (#6028)", () => {
+ const root = fixtureRoot();
+ const outputPath = "apps/loopover-ui/src/lib/ams-env-reference.ts";
+ writeMinerEnvReferenceModule({ rootDir: root, outputPath });
+ const first = readFileSync(join(root, outputPath), "utf8");
+ writeMinerEnvReferenceModule({ rootDir: root, outputPath });
+ const second = readFileSync(join(root, outputPath), "utf8");
+ expect(second).toBe(first);
+ });
});