diff --git a/packages/loopover-miner/DEPLOYMENT.md b/packages/loopover-miner/DEPLOYMENT.md index ea9ff90027..e0d167e945 100644 --- a/packages/loopover-miner/DEPLOYMENT.md +++ b/packages/loopover-miner/DEPLOYMENT.md @@ -77,6 +77,23 @@ For provider selection and the CLI-specific model/timeout overrides, see `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` — to relocate an individual file. `doctor`'s `store-integrity:*` checks report the persistent stores, so it is the quickest way to confirm what exists and is readable on disk. + **Other operator env knobs** (catalog also in [`docs/env-reference.md`](docs/env-reference.md)): + + - `LOOPOVER_MINER_LOG_LEVEL` — log verbosity (`debug` / `info` / `warn` / `error`; also `--verbose` / `--quiet`). + - `LOOPOVER_MINER_NO_UPDATE_CHECK` — set to `1`/`true`/`yes` to skip the startup npm-registry version nudge + (also `--no-update-check`). + - `LOOPOVER_MINER_VERSION` — override the resolved release id (fleet image builds stamp this). + - `LOOPOVER_MINER_REPO_CLONE_DIR` — base directory for cloned evaluation repos (`lib/repo-clone.js`). + - `LOOPOVER_MINER_WORKTREE_DIR` — base directory for per-attempt git worktrees (`lib/worktree-allocator.js`). + - `LOOPOVER_MINER_LEDGER_RETENTION_DAYS` / `LOOPOVER_MINER_LEDGER_RETENTION_MAX_ROWS` — opt-in age- and/or + size-based pruning for the append-only event/governor/prediction ledgers (unset ⇒ retention off). + - `LOOPOVER_MINER_CHAT_ACTIONS` — flag that enables chat-action dispatch (`lib/chat-action-dispatch.js`). + - `LOOPOVER_MINER_AMS_POLICY_PATH` — override path for the local AMS policy file (`lib/ams-policy.js`). + - `LOOPOVER_MINER_AMS_COLLECTOR_URL` / `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` — Orb export collector endpoint + and optional bearer credential (`lib/orb-export.js`). + - `LOOPOVER_MINER_SENTRY_DSN` / `LOOPOVER_MINER_SENTRY_ENVIRONMENT` — opt-in Sentry error tracking (no-op + unless the DSN is set; environment defaults to `production`). + 4. Optional per-repo miner goals: copy [`.loopover-miner.yml.example`](../../.loopover-miner.yml.example) to a target repo as `.loopover-miner.yml`. See [`docs/miner-goal-spec.md`](docs/miner-goal-spec.md). ## Fleet mode walkthrough diff --git a/packages/loopover-miner/lib/deployment-docs-audit.d.ts b/packages/loopover-miner/lib/deployment-docs-audit.d.ts index a2d4a3474c..2f4290ed83 100644 --- a/packages/loopover-miner/lib/deployment-docs-audit.d.ts +++ b/packages/loopover-miner/lib/deployment-docs-audit.d.ts @@ -8,6 +8,8 @@ export type DeploymentDocsClaims = { /** Filesystem-independent view of the live source tree the parsed claims are checked against. */ export type DeploymentDocsReality = { hasEnvRead: (name: string) => boolean; + /** Every scanned LOOPOVER_MINER_* / MINER_* token from miner+engine source (#6601 reverse check). */ + 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..d8d497ba97 100644 --- a/packages/loopover-miner/lib/deployment-docs-audit.js +++ b/packages/loopover-miner/lib/deployment-docs-audit.js @@ -1,7 +1,8 @@ -// Docs-accuracy audit for the miner's DEPLOYMENT.md (#5180). Mirrors the self-host docs audit +// Docs-accuracy audit for the miner's DEPLOYMENT.md (#5180, #6601). Mirrors the self-host docs audit // (apps/loopover-ui/src/lib/selfhost-docs-audit.ts): parse the deployment doc, then assert every // LOOPOVER_MINER_* / MINER_* env var, repo-relative file path, and `loopover-miner ` -// it documents still exists under packages/loopover-miner/**. A rename or move that leaves the doc +// it documents still exists under packages/loopover-miner/** — and the reverse for non-_DB +// LOOPOVER_MINER_* reads that the doc never mentions (#6601). A rename or move that leaves the doc // stale then fails CI with a message naming the exact stale claim, instead of misleading operators. // Wired into CI via `npm run test:miner-deployment-docs-audit` (scripts/check-miner-deployment-docs.mjs) // and the live unit suite in test/unit/miner-deployment-docs-audit.test.ts (#6158). @@ -75,11 +76,19 @@ export function scanRegisteredCommands(binSource) { } /** - * Cross-check parsed DEPLOYMENT.md claims against reality. `reality` supplies three predicates so this - * comparison stays pure and filesystem-independent: `hasEnvRead(name)` (a read of that env var exists - * under packages/loopover-miner/**), `pathExists(relativePath)` (the doc-relative path is on disk), - * and `isRegisteredCommand(name)` (the subcommand is dispatched by the CLI). Returns the drift findings, - * each failure naming the specific stale claim rather than a generic mismatch. + * Cross-check parsed DEPLOYMENT.md claims against reality. `reality` supplies three predicates plus + * an enumerable `envReads` set so this comparison stays pure and filesystem-independent: + * `hasEnvRead(name)` (a read of that env var exists under packages/loopover-miner/**), + * `envReads` (every scanned LOOPOVER_MINER_* / MINER_* token — used for the reverse check in #6601), + * `pathExists(relativePath)` (the doc-relative path is on disk), and `isRegisteredCommand(name)` + * (the subcommand is dispatched by the CLI). Returns the drift findings, each failure naming the + * specific stale claim rather than a generic mismatch. + * + * Reverse env-var check (#6601): every `LOOPOVER_MINER_*` token in `envReads` that does NOT end in + * `_DB` and is absent from `claims.envVars` is a failure. Tokens ending in `_DB` are exempt because + * DEPLOYMENT.md documents that family generically via `LOOPOVER_MINER__DB`. Bare `MINER_*` + * aliases (no `LOOPOVER_MINER_` prefix) are not included in the reverse direction — only the forward + * "documented claim must have a real read" check applies to them. */ export function auditDeploymentDocs(claims, reality) { const failures = []; @@ -90,6 +99,15 @@ export function auditDeploymentDocs(claims, reality) { ); } } + const documentedEnvVars = new Set(claims.envVars); + for (const name of reality.envReads) { + if (!name.startsWith("LOOPOVER_MINER_")) continue; + if (name.endsWith("_DB")) continue; + if (documentedEnvVars.has(name)) continue; + failures.push( + `env var "${name}" is read under packages/loopover-miner/** but is not documented in DEPLOYMENT.md`, + ); + } for (const path of claims.filePaths) { if (!reality.pathExists(path)) { failures.push(`file path "${path}" is linked from DEPLOYMENT.md but no longer exists on disk`); diff --git a/scripts/check-miner-deployment-docs.mjs b/scripts/check-miner-deployment-docs.mjs index 7941e7da2c..7b1fbacad0 100644 --- a/scripts/check-miner-deployment-docs.mjs +++ b/scripts/check-miner-deployment-docs.mjs @@ -39,6 +39,8 @@ export function buildLiveMinerDeploymentReality() { const registered = scanRegisteredCommands(readFileSync(BIN_ENTRY, "utf8")); return { hasEnvRead: (name) => envReads.has(name), + // Enumerable copy of the same scan `hasEnvRead` queries — reverse undocumented-env check (#6601). + 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..f41f826f52 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,18 @@ describe("check-miner-deployment-docs (#6158)", () => { expect(result.claimCounts.subcommands).toBeGreaterThan(0); }); + it("populates envReads on live reality and the full bidirectional audit passes (#6601)", () => { + const reality = buildLiveMinerDeploymentReality(); + const envReads = [...reality.envReads]; + expect(envReads.length).toBeGreaterThan(0); + expect(envReads.some((name) => name.startsWith("LOOPOVER_MINER_"))).toBe(true); + expect(typeof reality.hasEnvRead).toBe("function"); + // Same reality object the CI script builds — full audit (forward + reverse) must stay green. + const result = runMinerDeploymentDocsAudit({ reality }); + expect(result.ok).toBe(true); + expect(result.failures).toEqual([]); + }); + 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..de68befc06 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, }; @@ -162,6 +164,49 @@ describe("loopover-miner DEPLOYMENT.md docs-accuracy audit (#5180)", () => { expect(result.failures[0]).toContain("no read"); }); + it("flags an undocumented non-_DB LOOPOVER_MINER_* real read by name (#6601)", () => { + 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("not documented"); + }); + + it("does not flag a documented LOOPOVER_MINER_* real read (#6601)", () => { + const result = auditDeploymentDocs( + { envVars: ["LOOPOVER_MINER_LOG_LEVEL"], filePaths: [], subcommands: [] }, + { + ...ALWAYS_IN_SYNC, + hasEnvRead: (name) => name === "LOOPOVER_MINER_LOG_LEVEL", + envReads: ["LOOPOVER_MINER_LOG_LEVEL"], + }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + + it("does not flag an undocumented LOOPOVER_MINER_*_DB token (generic-pattern exemption) (#6601)", () => { + // DEPLOYMENT.md covers the per-store family via LOOPOVER_MINER__DB — reverse check must not + // force exhaustive per-store enumeration of that already-documented pattern. + const result = auditDeploymentDocs( + { envVars: [], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["LOOPOVER_MINER_PORTFOLIO_QUEUE_DB"] }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + + it("does not flag an undocumented bare MINER_* token without LOOPOVER_MINER_ prefix (#6601)", () => { + // Bare MINER_* matches non-env identifiers (event types, metrics, filenames); reverse check scopes + // to LOOPOVER_MINER_* only. Forward direction (documented MINER_* must have a real read) is unchanged. + const result = auditDeploymentDocs( + { envVars: [], filePaths: [], subcommands: [] }, + { ...ALWAYS_IN_SYNC, envReads: ["MINER_PR_OUTCOME_EVENT"] }, + ); + expect(result).toEqual({ ok: true, failures: [] }); + }); + it("flags a documented file path that no longer exists on disk", () => { const result = auditDeploymentDocs( { envVars: [], filePaths: ["docker-compose.moved.yml"], subcommands: [] }, @@ -188,7 +233,12 @@ describe("loopover-miner DEPLOYMENT.md docs-accuracy audit (#5180)", () => { 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/); });