Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ jobs:
- name: Miner package check
if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' }}
run: npm run test:miner-pack
- name: Miner deployment docs audit
if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' }}
run: npm run test:miner-deployment-docs-audit
# review-enrichment is not an npm workspace member (its own package-lock.json), so it needs its own
# cache entry -- same restore/save-after-success pattern and fork/trusted key split as the root
# install above, for the same reasons.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"build:miner": "npm --workspace @loopover/engine run build && npm --workspace @loopover/miner run build",
"test:mcp-pack": "node scripts/check-mcp-package.mjs",
"test:miner-pack": "node scripts/check-miner-package.mjs",
"test:miner-deployment-docs-audit": "node scripts/check-miner-deployment-docs.mjs",
"rees:install": "npm ci --prefix review-enrichment --prefer-offline --no-audit --no-fund",
"rees:test": "npm run rees:install && npm --prefix review-enrichment test",
"rees:metadata": "npm --prefix review-enrichment run metadata",
Expand Down Expand Up @@ -91,7 +92,7 @@
"test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs",
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci",
"test:watch": "vitest",
Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/deployment-docs-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// LOOPOVER_MINER_* / MINER_* env var, repo-relative file path, and `loopover-miner <subcommand>`
// it documents still exists under packages/loopover-miner/**. 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).

/** The miner's own env-var namespace: LOOPOVER_MINER_* and the shorter MINER_* aliases it reads. */
const ENV_VAR_PATTERN = /\b(?:LOOPOVER_MINER|MINER)_[A-Z0-9_]+\b/g;
Expand Down
29 changes: 29 additions & 0 deletions scripts/check-miner-deployment-docs.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type MinerDeploymentReality = {
hasEnvRead: (name: string) => boolean;
pathExists: (relativePath: string) => boolean;
isRegisteredCommand: (name: string) => boolean;
};

export type MinerDeploymentAuditResult = {
ok: boolean;
failures: string[];
claimCounts: {
envVars: number;
filePaths: number;
subcommands: number;
};
};

export function buildLiveMinerDeploymentReality(): MinerDeploymentReality;
export function runMinerDeploymentDocsAudit(opts?: {
testMode?: string | null;
reality?: MinerDeploymentReality;
}): MinerDeploymentAuditResult;

export type MinerDeploymentAuditIo = {
log: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
exit: (code: number) => void;
};

export function main(env?: Record<string, string | undefined>, io?: MinerDeploymentAuditIo): number;
99 changes: 99 additions & 0 deletions scripts/check-miner-deployment-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// CI entrypoint for the miner DEPLOYMENT.md accuracy audit (#6158). The pure checker lives in
// packages/loopover-miner/lib/deployment-docs-audit.js; this script builds live reality from the
// miner + engine trees and fails non-zero on drift so validate-code / test:ci catch renames.
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
assertDeploymentDocsInSync,
extractEnvVarClaims,
extractFilePathClaims,
extractSubcommandClaims,
scanEnvVarTokens,
scanRegisteredCommands,
} from "../packages/loopover-miner/lib/deployment-docs-audit.js";

const REPO_ROOT = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
const MINER_DIR = resolve(REPO_ROOT, "packages/loopover-miner");
const DEPLOYMENT_MD = resolve(MINER_DIR, "DEPLOYMENT.md");
const BIN_DIR = resolve(MINER_DIR, "bin");
const BIN_ENTRY = resolve(BIN_DIR, "loopover-miner.js");
const LIB_DIR = resolve(MINER_DIR, "lib");
const ENGINE_MINER_DIR = resolve(REPO_ROOT, "packages/loopover-engine/src/miner");

function readFilesWithExtension(dir, extension) {
return readdirSync(dir)
.filter((name) => name.endsWith(extension))
.map((name) => readFileSync(join(dir, name), "utf8"));
}

/** Build the live reality predicates used by the audit (exported for unit tests). */
export function buildLiveMinerDeploymentReality() {
const envReads = scanEnvVarTokens(
[
...readFilesWithExtension(LIB_DIR, ".js"),
...readFilesWithExtension(BIN_DIR, ".js"),
...readFilesWithExtension(ENGINE_MINER_DIR, ".ts"),
].join("\n"),
);
const registered = scanRegisteredCommands(readFileSync(BIN_ENTRY, "utf8"));
return {
hasEnvRead: (name) => envReads.has(name),
pathExists: (relativePath) => existsSync(resolve(MINER_DIR, relativePath)),
isRegisteredCommand: (name) => registered.has(name),
};
}

/**
* Run the live DEPLOYMENT.md audit.
* @param {{ testMode?: string | null, reality?: ReturnType<typeof buildLiveMinerDeploymentReality> }} [opts]
*/
export function runMinerDeploymentDocsAudit(opts = {}) {
const markdown = readFileSync(DEPLOYMENT_MD, "utf8");
const claims = {
envVars: extractEnvVarClaims(markdown),
filePaths: extractFilePathClaims(markdown),
subcommands: extractSubcommandClaims(markdown),
};
let reality = opts.reality ?? buildLiveMinerDeploymentReality();
if (opts.testMode === "missing-env") {
const inner = reality;
reality = {
...inner,
hasEnvRead: () => false,
};
}
const result = assertDeploymentDocsInSync(claims, reality);
return {
ok: result.ok,
failures: result.failures,
claimCounts: {
envVars: claims.envVars.length,
filePaths: claims.filePaths.length,
subcommands: claims.subcommands.length,
},
};
}

export function main(env = process.env, io = {
log: console.log.bind(console),
error: console.error.bind(console),
exit: (code) => process.exit(code),
}) {
try {
const result = runMinerDeploymentDocsAudit({
testMode: env.CHECK_MINER_DEPLOYMENT_DOCS_AUDIT_TEST_MODE ?? null,
});
io.log(
`Miner deployment docs audit ok: ${result.claimCounts.envVars} env vars, ${result.claimCounts.filePaths} paths, ${result.claimCounts.subcommands} subcommands.`,
);
return 0;
} catch (error) {
io.error(error instanceof Error ? error.message : String(error));
io.exit(1);
return 1;
}
}

const invokedDirectly = process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href;
if (invokedDirectly) main();
45 changes: 45 additions & 0 deletions test/unit/check-miner-deployment-docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it, vi } from "vitest";
import {
main,
runMinerDeploymentDocsAudit,
} from "../../scripts/check-miner-deployment-docs.mjs";

describe("check-miner-deployment-docs (#6158)", () => {
it("passes against the live miner DEPLOYMENT.md and source tree", () => {
const result = runMinerDeploymentDocsAudit();
expect(result.ok).toBe(true);
expect(result.failures).toEqual([]);
expect(result.claimCounts.envVars).toBeGreaterThan(0);
expect(result.claimCounts.filePaths).toBeGreaterThan(0);
expect(result.claimCounts.subcommands).toBeGreaterThan(0);
});

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);
});

it("main exits non-zero on the forced-missing-env drift fixture", () => {
const exit = vi.fn();
const error = vi.fn();
const log = vi.fn();
const code = main(
{ CHECK_MINER_DEPLOYMENT_DOCS_AUDIT_TEST_MODE: "missing-env" },
{ log, error, exit },
);
expect(code).toBe(1);
expect(exit).toHaveBeenCalledWith(1);
expect(error).toHaveBeenCalledWith(expect.stringMatching(/DEPLOYMENT\.md is out of sync/i));
expect(log).not.toHaveBeenCalled();
});

it("main prints ok and returns 0 on the live tree", () => {
const exit = vi.fn();
const error = vi.fn();
const log = vi.fn();
const code = main({}, { log, error, exit });
expect(code).toBe(0);
expect(exit).not.toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(expect.stringMatching(/^Miner deployment docs audit ok:/));
});
});
Loading