From b586810665222a673f5f0b6268bbe827156f3ca5 Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:57:19 +1000 Subject: [PATCH] fix(miner): flag real LOOPOVER_MINER_* env vars missing from DEPLOYMENT.md (#6601) deployment-docs-audit only checked the forward direction (a documented env var must have a real read). Add the reverse: a real LOOPOVER_MINER_*-prefixed read (excluding the *_DB family, documented generically, and the bare MINER_* alias namespace, which matches non-env constants) that DEPLOYMENT.md never documents now fails the audit by name. Expose reality.envReads as an enumerable set (both .d.ts and .d.mts) so the check can diff reality's own vars, populated from the existing scanEnvVarTokens result with no new scanning. Document the 13 previously-undocumented env vars in DEPLOYMENT.md so it passes the new check. Closes #6601 --- packages/loopover-miner/DEPLOYMENT.md | 18 +++++++++ .../lib/deployment-docs-audit.d.ts | 3 ++ .../lib/deployment-docs-audit.js | 12 ++++++ scripts/check-miner-deployment-docs.d.mts | 2 + scripts/check-miner-deployment-docs.mjs | 3 ++ test/unit/check-miner-deployment-docs.test.ts | 11 ++++++ test/unit/miner-deployment-docs-audit.test.ts | 39 ++++++++++++++++++- 7 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/loopover-miner/DEPLOYMENT.md b/packages/loopover-miner/DEPLOYMENT.md index ea9ff90027..40e2ffd6d8 100644 --- a/packages/loopover-miner/DEPLOYMENT.md +++ b/packages/loopover-miner/DEPLOYMENT.md @@ -186,6 +186,24 @@ See [`docs/operations-runbook.md`](docs/operations-runbook.md) for operational s See [`docs/sizing.md`](docs/sizing.md) for measured CPU/RAM/disk numbers for laptop mode vs. fleet mode at different worker counts, with the exact commands used to reproduce them. +## Environment variable reference + +All optional. The per-store `LOOPOVER_MINER__DB` path overrides (e.g. `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB`) +and `LOOPOVER_MINER_CONFIG_DIR` are covered above under the fleet/state notes; the rest are: + +| Variable | Read by | Purpose | +| --- | --- | --- | +| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.js` | Path to the operator's `.loopover-ams.yml` policy spec. | +| `LOOPOVER_MINER_AMS_COLLECTOR_URL`, `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.js` | Endpoint + bearer for pushing fleet state to an AMS/Orb collector. | +| `LOOPOVER_MINER_CHAT_ACTIONS` | `lib/chat-action-dispatch.js` | Truthy string enables the chat-action-dispatch flag (off by default). | +| `LOOPOVER_MINER_LEDGER_RETENTION_DAYS`, `LOOPOVER_MINER_LEDGER_RETENTION_MAX_ROWS` | `lib/store-maintenance.js` | Opt-in ledger retention window / row cap. | +| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.js` | Log verbosity override. | +| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | Truthy string disables the background update check. | +| `LOOPOVER_MINER_REPO_CLONE_DIR` | `lib/repo-clone.js` | Base directory for cloned target repos. | +| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.js` | Base directory for per-attempt git worktrees. | +| `LOOPOVER_MINER_SENTRY_DSN`, `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | Optional Sentry error reporting (DSN + environment tag). | +| `LOOPOVER_MINER_VERSION` | `lib/version.js` | Overrides the reported release id (e.g. for a custom build). | + ## Optional hosted discovery plane (opt-in) The Phase 6 **hosted discovery-index** is **off by default** — unlike Orb fleet export (`ORB_AIR_GAP` is the only opt-out). Operators who want cross-fleet metadata queries or soft-claim coordination must opt in explicitly. See [`docs/discovery-plane-operator-guide.md`](docs/discovery-plane-operator-guide.md) ([#4309](https://github.com/JSONbored/gittensory/issues/4309), placeholder until [#4300](https://github.com/JSONbored/gittensory/issues/4300) / [#4301](https://github.com/JSONbored/gittensory/issues/4301) / [#4302](https://github.com/JSONbored/gittensory/issues/4302) ship). diff --git a/packages/loopover-miner/lib/deployment-docs-audit.d.ts b/packages/loopover-miner/lib/deployment-docs-audit.d.ts index a2d4a3474c..94206f82a2 100644 --- a/packages/loopover-miner/lib/deployment-docs-audit.d.ts +++ b/packages/loopover-miner/lib/deployment-docs-audit.d.ts @@ -8,6 +8,9 @@ export type DeploymentDocsClaims = { /** Filesystem-independent view of the live source tree the parsed claims are checked against. */ export type DeploymentDocsReality = { hasEnvRead: (name: string) => boolean; + /** The enumerable set of every real env-var read, so the reverse audit direction (#6601) can diff a real + * `LOOPOVER_MINER_*` read that DEPLOYMENT.md never documents, not just probe one documented name at a time. */ + envReads: Iterable; pathExists: (relativePath: string) => boolean; isRegisteredCommand: (name: string) => boolean; }; diff --git a/packages/loopover-miner/lib/deployment-docs-audit.js b/packages/loopover-miner/lib/deployment-docs-audit.js index 49d5b0686e..88b754f4e3 100644 --- a/packages/loopover-miner/lib/deployment-docs-audit.js +++ b/packages/loopover-miner/lib/deployment-docs-audit.js @@ -102,6 +102,18 @@ export function auditDeploymentDocs(claims, reality) { ); } } + // Reverse direction (#6601): a real `LOOPOVER_MINER_*` env-var read that DEPLOYMENT.md never documents. Scoped + // to the `LOOPOVER_MINER_` prefix and excluding the `*_DB` family (documented generically via one pattern + // sentence, not enumerated) and the bare `MINER_*` alias namespace (which also matches non-env event/metric/ + // filename constants). `reality.envReads` is the enumerable set of real reads the forward `hasEnvRead` probes. + const documented = new Set(claims.envVars); + for (const name of reality.envReads) { + if (name.startsWith("LOOPOVER_MINER_") && !name.endsWith("_DB") && !documented.has(name)) { + failures.push( + `env var "${name}" is read under packages/loopover-miner/** but is not documented in DEPLOYMENT.md`, + ); + } + } return { ok: failures.length === 0, failures }; } diff --git a/scripts/check-miner-deployment-docs.d.mts b/scripts/check-miner-deployment-docs.d.mts index a6af3a3fa8..40ac0e7f9a 100644 --- a/scripts/check-miner-deployment-docs.d.mts +++ b/scripts/check-miner-deployment-docs.d.mts @@ -1,5 +1,7 @@ export type MinerDeploymentReality = { hasEnvRead: (name: string) => boolean; + /** Enumerable set of every real env-var read, for the reverse audit direction (#6601). */ + envReads: Iterable; pathExists: (relativePath: string) => boolean; isRegisteredCommand: (name: string) => boolean; }; diff --git a/scripts/check-miner-deployment-docs.mjs b/scripts/check-miner-deployment-docs.mjs index 7941e7da2c..018642d712 100644 --- a/scripts/check-miner-deployment-docs.mjs +++ b/scripts/check-miner-deployment-docs.mjs @@ -39,6 +39,9 @@ export function buildLiveMinerDeploymentReality() { const registered = scanRegisteredCommands(readFileSync(BIN_ENTRY, "utf8")); return { hasEnvRead: (name) => envReads.has(name), + // The full enumerable read-set (#6601), so auditDeploymentDocs can diff the reverse direction — a real + // `LOOPOVER_MINER_*` read missing from DEPLOYMENT.md — not just probe one documented name at a time. + envReads, pathExists: (relativePath) => existsSync(resolve(MINER_DIR, relativePath)), isRegisteredCommand: (name) => registered.has(name), }; diff --git a/test/unit/check-miner-deployment-docs.test.ts b/test/unit/check-miner-deployment-docs.test.ts index 31fb99953d..251501ed5b 100644 --- a/test/unit/check-miner-deployment-docs.test.ts +++ b/test/unit/check-miner-deployment-docs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { + buildLiveMinerDeploymentReality, main, runMinerDeploymentDocsAudit, } from "../../scripts/check-miner-deployment-docs.mjs"; @@ -14,6 +15,16 @@ describe("check-miner-deployment-docs (#6158)", () => { expect(result.claimCounts.subcommands).toBeGreaterThan(0); }); + it("exposes the live env-var read set as an enumerable field for the reverse audit (#6601)", () => { + const reality = buildLiveMinerDeploymentReality(); + const reads = [...reality.envReads]; + // Populated, and includes a var the reverse check now requires DEPLOYMENT.md to document. + expect(reads.length).toBeGreaterThan(0); + expect(reads).toContain("LOOPOVER_MINER_LOG_LEVEL"); + // The enumerable set and the boolean probe agree. + expect(reads.every((name) => reality.hasEnvRead(name))).toBe(true); + }); + it("fails when env-var backing reads are forced missing (drift fixture)", () => { expect(() => runMinerDeploymentDocsAudit({ testMode: "missing-env" })).toThrow(/DEPLOYMENT\.md is out of sync/i); }); diff --git a/test/unit/miner-deployment-docs-audit.test.ts b/test/unit/miner-deployment-docs-audit.test.ts index d11104d524..bccdd947c1 100644 --- a/test/unit/miner-deployment-docs-audit.test.ts +++ b/test/unit/miner-deployment-docs-audit.test.ts @@ -43,6 +43,7 @@ function buildLiveReality(): DeploymentDocsReality { const registered = scanRegisteredCommands(readFileSync(BIN_ENTRY, "utf8")); return { hasEnvRead: (name) => envReads.has(name), + envReads, pathExists: (relativePath) => existsSync(resolve(MINER_DIR, relativePath)), isRegisteredCommand: (name) => registered.has(name), }; @@ -50,6 +51,7 @@ function buildLiveReality(): DeploymentDocsReality { const ALWAYS_IN_SYNC: DeploymentDocsReality = { hasEnvRead: () => true, + envReads: [], pathExists: () => true, isRegisteredCommand: () => true, }; @@ -184,11 +186,46 @@ describe("loopover-miner DEPLOYMENT.md docs-accuracy audit (#5180)", () => { expect(result.failures[0]).toContain("not registered"); }); + it("reverse (#6601): flags a real LOOPOVER_MINER_* read that DEPLOYMENT.md never documents", () => { + const result = auditDeploymentDocs( + { envVars: [], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["LOOPOVER_MINER_LOG_LEVEL"] }, + ); + expect(result.ok).toBe(false); + expect(result.failures).toHaveLength(1); + expect(result.failures[0]).toContain("LOOPOVER_MINER_LOG_LEVEL"); + expect(result.failures[0]).toContain("is not documented"); + }); + + it("reverse (#6601): does NOT flag a real read that IS documented", () => { + const result = auditDeploymentDocs( + { envVars: ["LOOPOVER_MINER_LOG_LEVEL"], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["LOOPOVER_MINER_LOG_LEVEL"] }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + + it("reverse (#6601): does NOT flag an undocumented LOOPOVER_MINER_*_DB read (generic-pattern exemption)", () => { + const result = auditDeploymentDocs( + { envVars: [], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["LOOPOVER_MINER_PORTFOLIO_QUEUE_DB"] }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + + it("reverse (#6601): does NOT flag an undocumented bare MINER_* read (no LOOPOVER_MINER_ prefix)", () => { + const result = auditDeploymentDocs( + { envVars: [], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["MINER_PR_OUTCOME_EVENT"] }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + it("assertDeploymentDocsInSync throws and names every stale claim at once", () => { expect(() => assertDeploymentDocsInSync( { envVars: ["LOOPOVER_MINER_GONE"], filePaths: ["gone.yml"], subcommands: ["gone"] }, - { hasEnvRead: () => false, pathExists: () => false, isRegisteredCommand: () => false }, + { hasEnvRead: () => false, envReads: [], pathExists: () => false, isRegisteredCommand: () => false }, ), ).toThrow(/LOOPOVER_MINER_GONE[\s\S]*gone\.yml[\s\S]*loopover-miner gone/); });