diff --git a/.claude/skills/contributing-to-loopover/SKILL.md b/.claude/skills/contributing-to-loopover/SKILL.md index a67656b589..127392c3d3 100644 --- a/.claude/skills/contributing-to-loopover/SKILL.md +++ b/.claude/skills/contributing-to-loopover/SKILL.md @@ -191,6 +191,13 @@ This is where most PRs fail Codecov. The bar is **every changed line AND every c it is not wired into CI or the Codecov gate. Before pushing, run the whole suite **unsharded** — `npm run test:coverage` — the only faithful local coverage signal (CI shards + merges, so a single shard under-reports). + - **Touching `packages/loopover-{miner,mcp}`?** Run `npm run build:miner`/`build:mcp` once before + scoping to an individual test file. Most miner/mcp tests import `lib/*.js` in-process (resolves + straight to the `.ts`, no build needed) — but the shared CLI harnesses and every stdio-transport + test (`mcp-cli-*.test.ts`, `mcp-feasibility-gate.test.ts`, etc.) spawn the real compiled + `dist/bin/*.js`, and will fail with a confusing `ENOENT`/module-resolve error against a stale or + missing `dist/` if you skip this. Re-run the build after editing miner/mcp source, same as you'd + re-run any other build before testing its output. - **Find the uncovered branch.** In the v8 text report, read the **% Branch** column and the **Uncovered Line #s** for your changed file — a line at 100% lines but <100% branch has an un-taken `??`/ternary/`&&` side; add that case. Aim for **100% branch on your diff locally** so normal CI diff --git a/.claude/skills/contributing-to-loopover/reference.md b/.claude/skills/contributing-to-loopover/reference.md index 007188cd76..71b2836b3e 100644 --- a/.claude/skills/contributing-to-loopover/reference.md +++ b/.claude/skills/contributing-to-loopover/reference.md @@ -42,10 +42,16 @@ jobs run only if their path filter matched; on push to `main`, everything runs. | miner → pack | tarball hygiene | `npm run test:miner-pack` | unexpected/forbidden file in the miner npm tarball | `packages/loopover-{miner,mcp}` are `.ts`-only in git: editing their `bin`/`lib` source means editing `.ts` -and nothing else — the compiled `.js`/`.d.ts` these two `build` commands produce is gitignored, never -committed, and never something a PR needs to touch. Tests (in-process imports and the CLI-harness -subprocess spawns alike) run straight off the `.ts`; `build:mcp`/`build:miner` above exist only to -validate the real publishable artifact still compiles and packs cleanly. +and nothing else — the compiled `.js`/`.d.ts` these two `build` commands produce lands in a gitignored +`dist/` (out-of-place, mirroring `packages/loopover-engine`'s own `src/` → `dist/` split), never committed, +and never something a PR needs to touch. Tests that **import** miner/mcp `lib/*.js` in-process (the vast +majority) resolve straight to the sibling `.ts` with no build needed. Tests that **spawn a real subprocess** +— the shared CLI harnesses (`test/unit/support/{miner,mcp}-cli-harness.ts`) and every `mcp-cli-*.test.ts`/ +`mcp-feasibility-gate.test.ts`-style file connecting a `StdioClientTransport` — run the actual compiled +`dist/bin/*.js`, so **`npm run build:mcp`/`build:miner` must run before `test:coverage`** for those to pass; +`npm run test:ci` already sequences this correctly, so this only bites if you invoke `vitest`/`test:coverage` +directly without it. `build:mcp`/`build:miner`'s pack-check role (validating the real publishable artifact +still compiles and packs cleanly) is unchanged. | rees → test | review-enrichment-service's own suite | `npm run rees:test` | any failing test under `review-enrichment/` | | ui → openapi drift | spec check | `npm run ui:openapi:check` | committed `openapi.json` is stale (run `npm run ui:openapi`) | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3dd654b10..844e56d9d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -544,11 +544,11 @@ jobs: run: npm run test:mcp-pack # Same "ships .ts only, this build is for the published npm tarball + the pack-check below, not for # tests" story as MCP's own "Build MCP" step above. Invokes turbo.json's @loopover/miner#build:tsc - # (cache: false -- historically to avoid a cache restore stomping this package's hand-written .js - # files while the #7290 migration was still in-flight; now that #7317 closed it out, every bin/lib - # file is compiler-owned and that original risk no longer applies, but re-enabling caching here is a - # separate change this PR doesn't make) and @loopover/miner#build:verify (real caching win: skips - # re-`node --check`-ing all 121 bin/lib files when neither changed) DIRECTLY as their own task names, + # (real caching win since the 2026-07-24 dist/ migration -- previously cache: false, historically to + # avoid a cache restore stomping this package's hand-written .js files while the #7290 migration was + # still in-flight; #7317 closed that out, and the dist/ split removed the original in-place-emit + # hazard entirely, so caching was finally enabled) and @loopover/miner#build:verify (real caching + # win: skips re-`node --check`-ing all bin/lib files when neither changed) DIRECTLY as their own task names, # deliberately NOT via the aggregate `@loopover/miner#build` task (which exists for standalone/local # `npm run build` callers) -- that aggregate's own script is `npm run build:tsc && npm run # build:verify`, i.e. it re-invokes both of these exact scripts a second time even when turbo already @@ -900,18 +900,17 @@ jobs: - name: Build engine package run: npx turbo run build --filter=@loopover/engine # packages/loopover-mcp and packages/loopover-miner both ship .ts source only (no compiled .js - # committed -- gitignored, same as @loopover/engine's dist/), but this job's own SKIP_MCP_CLI_HARNESS/ - # SKIP_MINER_TEST_HARNESS env below (see "Test with coverage") can both be false, which spawns the - # REAL built CLI binary (packages/loopover-mcp/bin/loopover-mcp.js / packages/loopover-miner/bin/ - # loopover-miner.js) as a subprocess -- AND, separately, every mcp-cli-*/miner-mcp-* test that instead - # runs the .ts entry directly via `node --experimental-strip-types` (test/unit/support/mcp-cli-harness.ts - # et al.) still needs this: that flag only strips types from the directly-executed entry file, it does - # NOT make plain Node resolve a `.js`-suffixed relative import to a sibling .ts the way Vite/esbuild do - # (confirmed live: `node --experimental-strip-types packages/loopover-mcp/bin/loopover-mcp.ts doctor` - # with no compiled lib/ present fails ERR_MODULE_NOT_FOUND on lib/local-branch.js, the CLI's own first - # local import) -- so bin/lib's compiled .js has to physically exist on disk either way. Before the - # compiled output stopped being committed, this job's checkout already had it for free; now it needs - # building here too, same as validate-code's own "Build MCP"/"Build miner CLI" steps. + # committed -- gitignored, and since the 2026-07-24 dist/ migration built out-of-place into dist/, + # same as @loopover/engine's own dist/; see each package's tsconfig.json outDir comment). Every + # mcp-cli-*/miner-*/mcp-feasibility-gate-style test spawning a real CLI subprocess -- whether via + # the shared test/unit/support/{mcp,miner}-cli-harness.ts or its own hand-rolled StdioClientTransport + # -- runs the real compiled dist/bin/*.js directly via plain `node` now (no `--experimental-strip- + # types`; that was only ever viable while the compiled .js sat in-place next to the .ts source for + # Node's own resolver to find its internal lib/*.js imports, which stopped being true once dist/ + # physically separated them -- see the harness files' own comments). So bin/lib's compiled output has + # to physically exist on disk before any of those tests run. Before the compiled output stopped being + # committed, this job's checkout already had it for free; now it needs building here too, same as + # validate-code's own "Build MCP"/"Build miner CLI" steps. # # --force on both: turbo.json's @loopover/mcp#build task now declares real `outputs` (see its own # comment) so a cache HIT restores the actual files instead of only the log, but this job's 3 parallel @@ -937,7 +936,7 @@ jobs: - name: Verify MCP/miner CLI binaries were actually built run: | set -euo pipefail - for f in packages/loopover-mcp/bin/loopover-mcp.js packages/loopover-miner/bin/loopover-miner.js; do + for f in packages/loopover-mcp/dist/bin/loopover-mcp.js packages/loopover-miner/dist/bin/loopover-miner.js; do if [ ! -f "$f" ]; then echo "::error::$f is missing after Build MCP/Build miner CLI both ran with --force. Every mcp-cli-*/miner-* test below will fail with a confusing MODULE_NOT_FOUND/ENOENT instead of this message -- something deeper changed (e.g. the build script itself stopped writing to this path); this is not a cache/timing issue, --force rules that out." exit 1 diff --git a/.gitignore b/.gitignore index c6529e1896..35a55cdfd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,15 @@ node_modules dist/ dist-ssr/ -# packages/loopover-{miner,mcp} compile real TypeScript in place (tsc's outDir === rootDir) and never -# commit the emitted output -- both ship as installable CLIs, so the .js still has to exist in the -# published npm tarball (built fresh by `npm run build:{miner,mcp}` / the publish workflows), but nothing -# else needs a prior build: Vite/esbuild resolve the .js-suffixed import specifiers these packages write -# straight to the sibling .ts by default (same as packages/loopover-engine/src/**, which has never had a -# committed .js either), and the CLI-harness tests spawn the .ts directly via Node's own type-stripping. -packages/loopover-miner/bin/*.js -packages/loopover-miner/bin/*.d.ts -packages/loopover-miner/lib/*.js -packages/loopover-miner/lib/*.d.ts -packages/loopover-mcp/bin/*.js -packages/loopover-mcp/lib/*.js +# packages/loopover-{miner,mcp} compile real TypeScript out-of-place into dist/ (tsc's outDir "dist", +# matching packages/loopover-engine's src/ -> dist/ split) and never commit the emitted output -- both +# ship as installable CLIs, so the .js still has to exist in the published npm tarball (built fresh by +# `npm run build:{miner,mcp}` / the publish workflows), but nothing else needs a prior build: Vite/esbuild +# resolve the .js-suffixed import specifiers these packages write straight to the sibling .ts by default +# (same as packages/loopover-engine/src/**, which has never had a committed .js either), and the +# CLI-harness tests spawn the .ts directly via Node's own type-stripping. The generic "dist/" pattern +# above already covers packages/loopover-{miner,mcp}/dist/** -- no package-specific line needed (this was +# in-place emit with its own explicit ignore lines here until the 2026-07-24 migration). .turbo/ .output .vinxi diff --git a/Dockerfile b/Dockerfile index ed371d4e47..4ffe7db2e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ WORKDIR /app # (workspaces: apps/*, packages/*), and `npm ci` only symlinks node_modules/ to a workspace whose # directory already exists on disk. Copying just the root package*.json first (the usual dependency-layer # caching trick) left every workspace package.json missing at `npm ci` time, so npm silently skipped every -# internal symlink -- @loopover/engine (a workspace dependency of loopover-miner's checked-in +# internal symlink -- @loopover/engine (a workspace dependency of loopover-miner's built # lib/*.js artifacts, #2281) then couldn't be resolved by esbuild no matter how/when its own dist/ was built. COPY . . # --ignore-scripts: no native builds are needed (SQLite is the built-in node:sqlite; @hono/node-server is diff --git a/package-lock.json b/package-lock.json index e6da48f999..b569cd28c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21318,7 +21318,7 @@ "zod": "^4.4.3" }, "bin": { - "loopover-mcp": "bin/loopover-mcp.js" + "loopover-mcp": "dist/bin/loopover-mcp.js" }, "devDependencies": { "@types/node": "^22.20.0", @@ -21356,9 +21356,9 @@ "zod": "^4.4.3" }, "bin": { - "loopover-miner": "bin/loopover-miner.js", - "loopover-miner-hosted": "bin/loopover-miner-hosted.js", - "loopover-miner-mcp": "bin/loopover-miner-mcp.js" + "loopover-miner": "dist/bin/loopover-miner.js", + "loopover-miner-hosted": "dist/bin/loopover-miner-hosted.js", + "loopover-miner-mcp": "dist/bin/loopover-miner-mcp.js" }, "devDependencies": { "@types/node": "^22.20.0", diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 610b2ba115..0fb025d8b1 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -48,10 +48,24 @@ import { redactKnownLocalPaths, redactLocalPath } from "../lib/redact-local-path // side by side unaliased would read as the same function (#6238). import { recordMcpToolCall as recordLocalMcpToolCall } from "../lib/telemetry.js"; +// Self-referencing package import (Node's own mechanism for "resolve a file that belongs to my own +// package, correctly, regardless of my own current location on disk") -- requires the "exports" map +// in this package's own package.json (the "./x": "./x" entries there are what make this resolvable +// at all). Robust by construction, not by guessing: whether this file is running as the real source +// bin/loopover-mcp.ts (imported in-process by tests that exercise the CLI dispatcher + stdio tools +// directly, per the invokedPath check below) or as the compiled dist/bin/loopover-mcp.js (a real CLI +// invocation), import.meta.resolve walks up from THIS file's own location through node_modules the +// same way any external consumer's "@loopover/mcp/..." import would, landing on the one real +// package.json/CHANGELOG.md either way -- no relative-depth arithmetic, so a future directory move +// can never silently break this again the way the pre-dist/-migration relative path did. +function resolveOwnPackageFile(specifier: string): URL { + return new URL(import.meta.resolve(specifier)); +} + // Read name/version from this package's own package.json (always present in any install -- // global, npx, or local -- npm ships it regardless of the "files" allowlist) instead of hand-synced // literals, so a release bump never has a second place to forget. -const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); +const ownPackageJson = JSON.parse(readFileSync(resolveOwnPackageFile("@loopover/mcp/package.json"), "utf8")); const defaultApiUrl = "https://api.loopover.ai"; const legacyDefaultApiUrls = new Set([ @@ -71,7 +85,7 @@ const decisionPackCacheSchemaVersion = 1; const decisionPackCacheMaxEntries = 25; const decisionPackCacheMaxBytes = 512 * 1024; const cliTextFileMaxBytes = 1024 * 1024; -const changelogPath = new URL("../CHANGELOG.md", import.meta.url); +const changelogPath = resolveOwnPackageFile("@loopover/mcp/CHANGELOG.md"); const cliArgs = process.argv.slice(2); // #7764: true only when this file is the process entrypoint (`node .../loopover-mcp.js`, incl. via the npm @@ -564,11 +578,30 @@ const feasibilityGateShape = { * `releaseClaim`, or `expireClaim` -- it never gains any ability to block, cancel, or override a claim or * attempt; real claim-conflict authority stays entirely with #4848's maintainer-only path. */ +/** The narrow slice of @loopover/miner's claim-ledger module this file actually calls -- hand-shaped + * rather than imported, so this optional cross-package integration carries no TYPE-level coupling to + * miner's internals either, matching its own already-optional RUNTIME shape (this module may not be + * installed alongside loopover-mcp at all). */ +type ClaimLedgerModule = { + resolveClaimLedgerDbPath: () => string; + openClaimLedgerReadOnly: (dbPath: string) => { + listActiveClaims: (repoFullName: string) => Array<{ issueNumber: number }>; + close: () => void; + }; +}; + async function resolveLedgerClaimStatus(repoFullName: any, issueNumber: any) { if (!repoFullName || !issueNumber) return null; - let claimLedgerModule; + let claimLedgerModule: ClaimLedgerModule; try { - claimLedgerModule = await import("@loopover/miner/lib/claim-ledger.js"); + // Specifier built from concatenated parts, not a literal, so tsc never attempts to statically + // resolve/typecheck the target module: this optional cross-package import must not create a + // BUILD-TIME dependency on @loopover/miner's own dist/ output existing yet (CI builds MCP before + // miner in validate-tests) or on @loopover/miner being installed/built at all -- both are real, + // already-supported states the try/catch below already handles at runtime. The `as unknown as` + // cast onto the hand-shaped type above is the only thing giving this call real types. + const specifier = "@loopover/miner" + "/lib/claim-ledger.js"; + claimLedgerModule = (await import(specifier)) as unknown as ClaimLedgerModule; } catch { /* v8 ignore next -- loopover-miner genuinely unresolvable (not installed alongside loopover-mcp); not reproducible in this monorepo's workspace-hoisted test environment, where the sibling package always diff --git a/packages/loopover-mcp/package.json b/packages/loopover-mcp/package.json index 930adad187..6bdaf21c7e 100644 --- a/packages/loopover-mcp/package.json +++ b/packages/loopover-mcp/package.json @@ -26,15 +26,16 @@ "access": "public" }, "bin": { - "loopover-mcp": "bin/loopover-mcp.js" + "loopover-mcp": "dist/bin/loopover-mcp.js" + }, + "exports": { + "./package.json": "./package.json", + "./CHANGELOG.md": "./CHANGELOG.md" }, "files": [ - "bin", - "lib", + "dist", "scripts", "CHANGELOG.md", - "!bin/**/*.ts", - "!lib/**/*.ts", "!scripts/check-syntax.mjs", "!scripts/strip-bin-sourcemap.mjs" ], diff --git a/packages/loopover-mcp/scripts/check-syntax.mjs b/packages/loopover-mcp/scripts/check-syntax.mjs index e9864d384c..863b943518 100644 --- a/packages/loopover-mcp/scripts/check-syntax.mjs +++ b/packages/loopover-mcp/scripts/check-syntax.mjs @@ -19,7 +19,7 @@ function listFiles(dir, extension) { .map((entry) => join(dir, entry.name)); } -const files = [...listFiles("bin", ".js"), ...listFiles("lib", ".js"), ...listFiles("scripts", ".mjs")].sort(); +const files = [...listFiles("dist/bin", ".js"), ...listFiles("dist/lib", ".js"), ...listFiles("scripts", ".mjs")].sort(); const failures = []; for (const file of files) { @@ -38,4 +38,4 @@ if (failures.length > 0) { process.exit(1); } -console.log(`node --check passed for all ${files.length} files in bin/, lib/, and scripts/.`); +console.log(`node --check passed for all ${files.length} files in dist/bin/, dist/lib/, and scripts/.`); diff --git a/packages/loopover-mcp/scripts/strip-bin-sourcemap.mjs b/packages/loopover-mcp/scripts/strip-bin-sourcemap.mjs index 8323afcf40..38de207e2e 100644 --- a/packages/loopover-mcp/scripts/strip-bin-sourcemap.mjs +++ b/packages/loopover-mcp/scripts/strip-bin-sourcemap.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node -// Strip the trailing inline sourceMappingURL from compiled bin/*.js after tsc. +// Strip the trailing inline sourceMappingURL from compiled dist/bin/*.js after tsc. // // The package tsconfig uses inlineSourceMap so small lib modules stay coverage-remapable without -// publishing a new *.map file type. For bin/loopover-mcp.js (~6.5k lines) the inline map roughly +// publishing a new *.map file type. For dist/bin/loopover-mcp.js (~6.5k lines) the inline map roughly // doubles the shipped file past LoopOver's patch-less secrets-scan fetch cap (512KB), which made // the prior Phase 3 attempt (#7431) fail closed. Bin is subprocess-only tested (mcp-cli harness), // so v8 never remaps through this file anyway — stripping the map keeps the published/committed @@ -12,7 +12,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); -const BIN = join(ROOT, "bin"); +const BIN = join(ROOT, "dist/bin"); const MARKER = "\n//# sourceMappingURL="; let stripped = 0; @@ -26,4 +26,4 @@ for (const entry of readdirSync(BIN, { withFileTypes: true })) { stripped += 1; } -console.log(`stripped inline sourcemaps from ${stripped} bin/*.js file(s)`); +console.log(`stripped inline sourcemaps from ${stripped} dist/bin/*.js file(s)`); diff --git a/packages/loopover-mcp/tsconfig.json b/packages/loopover-mcp/tsconfig.json index a67c879a19..3795e72d26 100644 --- a/packages/loopover-mcp/tsconfig.json +++ b/packages/loopover-mcp/tsconfig.json @@ -13,23 +13,23 @@ // v8 coverage provider still remaps through it fine, attributing coverage to the .ts source. "inlineSourceMap": true, "noEmit": false, - // Overrides the root config's "dist" so each converted file compiles in place next to its .ts - // source (e.g. lib/foo.ts -> lib/foo.js) -- the package's published bin/lib layout never changes - // as more files convert, so no consumer (in this repo or published) ever needs a different import - // path depending on a given file's migration status. + // Out-of-place emit, matching packages/loopover-engine's src/ -> dist/ split: lib/foo.ts -> dist/lib/foo.js, + // bin/foo.ts -> dist/bin/foo.js. Was in-place (outDir ".") until the 2026-07-24 migration -- a compiled + // .js sitting next to its .ts source in the same directory Vite/vitest also reads caused a real + // codecov/patch (0%/99%) incident (Vite resolved the literal .js over the .ts sibling, misattributing + // coverage and breaking --changed's import-graph tracing) and required a resolver-plugin workaround + // before this proper fix landed. rootDir "." (not "bin"/"lib") preserves the bin/ vs lib/ nesting inside + // dist/, so internal relative imports (e.g. bin/foo.ts's "../lib/bar.js") keep resolving correctly. "rootDir": ".", - "outDir": ".", + "outDir": "dist", // Without this, the inherited root value ("./.tsbuildinfo") resolves relative to the ROOT config's // location, not this one -- both packages would then read/write the exact same cache file at the // repo root and corrupt each other's incremental state. "tsBuildInfoFile": "./.tsbuildinfo" }, // Every bin/lib runtime module is real TypeScript (#7291, phased across #7328/#7329/#7330): tsc owns - // the in-place .js emit, which is gitignored -- contributors and tests never touch it (see - // .gitignore's comment on these paths). The glob stays the include surface as the package's only - // source of truth. + // the dist/ .js emit, which is gitignored -- contributors and tests never touch it. The glob stays + // the include surface as the package's only source of truth. "include": ["bin/**/*.ts", "lib/**/*.ts"], - // Without this, tsc's default exclude list (which always adds outDir) resolves to "." -- the whole - // package root -- and silently excludes every include match, since outDir is "." for in-place emit. "exclude": [] } diff --git a/packages/loopover-miner/DEPLOYMENT.md b/packages/loopover-miner/DEPLOYMENT.md index 0a2d7f2e88..dc914aa4ac 100644 --- a/packages/loopover-miner/DEPLOYMENT.md +++ b/packages/loopover-miner/DEPLOYMENT.md @@ -208,37 +208,37 @@ and `LOOPOVER_MINER_CONFIG_DIR` are covered above under the fleet/state notes; t | 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_NEON_API_KEY`, `LOOPOVER_MINER_NEON_PROJECT_ID`, `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `lib/attempt-db-fork-config.js` | Neon branch-per-attempt disposable DB fork ([#7858](https://github.com/JSONbored/loopover/issues/7858)). All three required together — any one missing disables the feature entirely (no branch is ever created). | -| `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). | +| `LOOPOVER_MINER_AMS_POLICY_PATH` | `dist/lib/ams-policy.js` | Path to the operator's `.loopover-ams.yml` policy spec. | +| `LOOPOVER_MINER_AMS_COLLECTOR_URL`, `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `dist/lib/orb-export.js` | Endpoint + bearer for pushing fleet state to an AMS/Orb collector. | +| `LOOPOVER_MINER_CHAT_ACTIONS` | `dist/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` | `dist/lib/store-maintenance.js` | Opt-in ledger retention window / row cap. | +| `LOOPOVER_MINER_LOG_LEVEL` | `dist/lib/logger.js` | Log verbosity override. | +| `LOOPOVER_MINER_NEON_API_KEY`, `LOOPOVER_MINER_NEON_PROJECT_ID`, `LOOPOVER_MINER_NEON_PARENT_BRANCH_ID` | `dist/lib/attempt-db-fork-config.js` | Neon branch-per-attempt disposable DB fork ([#7858](https://github.com/JSONbored/loopover/issues/7858)). All three required together — any one missing disables the feature entirely (no branch is ever created). | +| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `dist/lib/update-check.js` | Truthy string disables the background update check. | +| `LOOPOVER_MINER_REPO_CLONE_DIR` | `dist/lib/repo-clone.js` | Base directory for cloned target repos. | +| `LOOPOVER_MINER_WORKTREE_DIR` | `dist/lib/worktree-allocator.js` | Base directory for per-attempt git worktrees. | +| `LOOPOVER_MINER_SENTRY_DSN`, `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `dist/lib/sentry.js` | Optional Sentry error reporting (DSN + environment tag). | +| `LOOPOVER_MINER_VERSION` | `dist/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, via `lib/discovery-index-client.js` ([#7168](https://github.com/JSONbored/loopover/issues/7168), wired against the hosted server from [#7164](https://github.com/JSONbored/loopover/issues/7164)/[#7166](https://github.com/JSONbored/loopover/issues/7166)): +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, via `dist/lib/discovery-index-client.js` ([#7168](https://github.com/JSONbored/loopover/issues/7168), wired against the hosted server from [#7164](https://github.com/JSONbored/loopover/issues/7164)/[#7166](https://github.com/JSONbored/loopover/issues/7166)): | Variable | Read by | Purpose | | --- | --- | --- | -| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.js` | Master opt-in (truthy string, off by default). No hosted discovery-index traffic or telemetry unless set. | -| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.js` | Base URL of the hosted discovery-index service. Required for the plane to do anything once enabled. | -| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `lib/discovery-index-client.js` | Optional bearer secret for the hosted endpoint, if it requires one. | -| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `lib/discovery-index-client.js` | Second, independent opt-in for anonymized operational telemetry — can stay off while the plane itself is queried/claimed against. | +| `LOOPOVER_MINER_DISCOVERY_PLANE` | `dist/lib/discovery-index-client.js` | Master opt-in (truthy string, off by default). No hosted discovery-index traffic or telemetry unless set. | +| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `dist/lib/discovery-index-client.js` | Base URL of the hosted discovery-index service. Required for the plane to do anything once enabled. | +| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `dist/lib/discovery-index-client.js` | Optional bearer secret for the hosted endpoint, if it requires one. | +| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `dist/lib/discovery-index-client.js` | Second, independent opt-in for anonymized operational telemetry — can stay off while the plane itself is queried/claimed against. | See [`docs/discovery-plane-operator-guide.md`](docs/discovery-plane-operator-guide.md) for the full invariant list (metadata-only, no compensation signals, credentials stay local). ## Optional hosted control plane — tenant admin (opt-in) -The `loopover-miner tenant` command group (create/list/destroy) provisions hosted tenant instances against the ORB+AMS hosting control-plane's provisioning API ([#7275](https://github.com/JSONbored/loopover/issues/7275)). It is **off by default** and completely inert unless opted in; unlike the discovery plane, these are deliberate **admin actions that fail loud** (a disabled/unconfigured/unreachable/error condition exits non-zero rather than degrading silently), via `lib/tenant-client.js`: +The `loopover-miner tenant` command group (create/list/destroy) provisions hosted tenant instances against the ORB+AMS hosting control-plane's provisioning API ([#7275](https://github.com/JSONbored/loopover/issues/7275)). It is **off by default** and completely inert unless opted in; unlike the discovery plane, these are deliberate **admin actions that fail loud** (a disabled/unconfigured/unreachable/error condition exits non-zero rather than degrading silently), via `dist/lib/tenant-client.js`: | Variable | Read by | Purpose | | --- | --- | --- | -| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.js` | Master opt-in (truthy string, off by default). No tenant admin traffic is possible unless set. | -| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.js` | Base URL of the hosted control-plane provisioning API. Required once the plane is enabled. | -| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.js` | Bearer admin credential for the provisioning API — distinct from any tenant's own per-instance secrets. Required once the plane is enabled. | +| `LOOPOVER_MINER_CONTROL_PLANE` | `dist/lib/tenant-client.js` | Master opt-in (truthy string, off by default). No tenant admin traffic is possible unless set. | +| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `dist/lib/tenant-client.js` | Base URL of the hosted control-plane provisioning API. Required once the plane is enabled. | +| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `dist/lib/tenant-client.js` | Bearer admin credential for the provisioning API — distinct from any tenant's own per-instance secrets. Required once the plane is enabled. | diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.ts b/packages/loopover-miner/bin/loopover-miner-mcp.ts index b37eddd07e..92048056d2 100755 --- a/packages/loopover-miner/bin/loopover-miner-mcp.ts +++ b/packages/loopover-miner/bin/loopover-miner-mcp.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node import { readFileSync, realpathSync } from "node:fs"; -import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; @@ -52,12 +51,17 @@ import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; // from ORB's hosted, maintainer-authenticated loopover_get_outcome_calibration tool. // Read the version from this package's own package.json (always shipped) rather than a hand-synced -// literal, so a release bump never has a second place to forget -- same approach as the mcp harness. -// Resolve via fileURLToPath(import.meta.url) (a string) rather than `new URL(...)` so the path never -// materializes as a `URL` object -- the repo-root tsconfig this file is also checked under (its type -// surface is imported by the MCP unit tests) resolves the global `URL` to a shape whose iterator lacks -// `[Symbol.dispose]`, which readFileSync's node typings reject; a plain string sidesteps that entirely. -const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json"); +// literal, so a release bump never has a second place to forget. Self-referencing package import +// (requires the "exports" map in this package's own package.json) -- robust by construction to +// however this file is currently running, whether as the real source bin/loopover-miner-mcp.ts +// (imported in-process by test/unit/miner-mcp-*.test.ts) or the compiled dist/bin/loopover-miner-mcp.js +// (a real CLI invocation): import.meta.resolve walks up from THIS file's own location through +// node_modules the same way an external "@loopover/miner/..." import would, landing on the one real +// package.json either way -- no relative-path arithmetic to break if this file ever moves again. +// fileURLToPath (a plain string), not `new URL(...)` -- the repo-root tsconfig this file is also +// checked under (its type surface is imported by the MCP unit tests) resolves the global `URL` to a +// shape whose iterator lacks `[Symbol.dispose]`, which readFileSync's node typings reject. +const packageJsonPath = fileURLToPath(import.meta.resolve("@loopover/miner/package.json")); const ownPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); /** Optional filters accepted by loopover_miner_get_audit_feed (#5158). */ diff --git a/packages/loopover-miner/lib/status.ts b/packages/loopover-miner/lib/status.ts index d2e9f0b126..8c299dcb1c 100644 --- a/packages/loopover-miner/lib/status.ts +++ b/packages/loopover-miner/lib/status.ts @@ -50,6 +50,21 @@ function moduleDir(): string { return (cachedModuleDir ??= import.meta.dirname); } +// Monorepo-sibling fallback path (packages/loopover-engine, packages/loopover-miner's own +// expected-engine.version pin) computed relative to THIS module's own on-disk location -- which +// differs by one directory level depending on how this file is currently running: in-process test +// imports resolve moduleDir() to the real source lib/ (2 levels above packages/loopover-miner/), +// while a real CLI invocation resolves it to the compiled dist/lib/ (3 levels above, since the +// 2026-07-24 dist/ migration added one more level of nesting; see tsconfig.json's outDir comment). A +// single hardcoded relative depth can only ever be correct for one of those two contexts, so this +// tries both, preferring whichever the current on-disk layout actually has -- robust to either +// execution mode without needing to detect which one is active. +function resolveMonorepoSiblingPath(...segments: string[]): string { + const fromLib = join(moduleDir(), "..", ...segments); + if (existsSync(fromLib)) return fromLib; + return join(moduleDir(), "..", "..", ...segments); +} + const PACKAGE_NAME = "@loopover/miner"; const ENGINE_PACKAGE = "@loopover/engine"; // Config-file discovery order (mirrors the `.loopover-miner.yml` precedence the goal-spec parser documents). @@ -117,7 +132,7 @@ export function buildEngineVersionDisplay(readInstalled: () => string | null = r if (installed) return installed; try { /* v8 ignore next -- package.json declares @loopover/engine in every supported miner build */ - return (requireFromHere()("../package.json") as PackageJsonShape).dependencies?.[ENGINE_PACKAGE] ?? null; + return (requireFromHere()("@loopover/miner/package.json") as PackageJsonShape).dependencies?.[ENGINE_PACKAGE] ?? null; } catch { /* v8 ignore next -- import metadata/package resolution failure is bundler-only; normal Node tests resolve it */ return null; @@ -162,7 +177,7 @@ export function readInstalledEnginePackageVersion(): string | null { try { return readInstalledEnginePackageVersionFromPaths( requireFromHere().resolve(ENGINE_PACKAGE), - join(moduleDir(), "../../loopover-engine/package.json"), + resolveMonorepoSiblingPath("..", "loopover-engine", "package.json"), ); } catch { /* v8 ignore next 9 -- only reaches when Node cannot resolve the installed package at all */ @@ -205,7 +220,7 @@ export function readExpectedEnginePackageVersionFromPaths( export function readExpectedEnginePackageVersion(): string | null { return readExpectedEnginePackageVersionFromPaths( join(moduleDir(), "../../loopover-engine/package.json"), - join(moduleDir(), "../expected-engine.version"), + resolveMonorepoSiblingPath("expected-engine.version"), ); } @@ -260,7 +275,7 @@ function checkEngineVersionSkew(): DoctorCheck { /** The minimum Node major version from the package's `engines.node` floor (e.g. ">=22.13.0" → 22). */ function requiredNodeMajor(): number { - const engines = (requireFromHere()("../package.json") as PackageJsonShape).engines; + const engines = (requireFromHere()("@loopover/miner/package.json") as PackageJsonShape).engines; /* v8 ignore next -- package.json's required engines.node is a build-time invariant */ const match = typeof engines?.node === "string" ? engines.node.match(/(\d+)/) : null; /* v8 ignore next -- a matching engines.node floor always includes the captured major */ diff --git a/packages/loopover-miner/lib/version.ts b/packages/loopover-miner/lib/version.ts index b32adac6ee..3aed6b7a42 100644 --- a/packages/loopover-miner/lib/version.ts +++ b/packages/loopover-miner/lib/version.ts @@ -1,4 +1,10 @@ -import ownPackageJson from "../package.json" with { type: "json" }; +// Self-referencing package import (requires the "exports" map in this package's own package.json) -- +// robust by construction to however this file is currently running, whether as the real source +// lib/version.ts (imported in-process by tests) or the compiled dist/lib/version.js (a real CLI +// invocation): resolution walks up from THIS file's own location through node_modules the same way an +// external "@loopover/miner/..." import would, landing on the one real package.json either way -- no +// relative-path arithmetic to break if this file ever moves again. +import ownPackageJson from "@loopover/miner/package.json" with { type: "json" }; /** Package.json semver at import time — the laptop npm-install default. */ export const MINER_PACKAGE_VERSION: string = ownPackageJson.version; diff --git a/packages/loopover-miner/package.json b/packages/loopover-miner/package.json index c029b9c09a..885a9ddea7 100644 --- a/packages/loopover-miner/package.json +++ b/packages/loopover-miner/package.json @@ -23,22 +23,21 @@ "access": "public" }, "bin": { - "loopover-miner": "bin/loopover-miner.js", - "loopover-miner-mcp": "bin/loopover-miner-mcp.js", - "loopover-miner-hosted": "bin/loopover-miner-hosted.js" + "loopover-miner": "dist/bin/loopover-miner.js", + "loopover-miner-mcp": "dist/bin/loopover-miner-mcp.js", + "loopover-miner-hosted": "dist/bin/loopover-miner-hosted.js" + }, + "exports": { + "./package.json": "./package.json", + "./lib/claim-ledger.js": "./dist/lib/claim-ledger.js" }, "files": [ - "bin", - "lib", + "dist", "docs", "schema", "DEPLOYMENT.md", "Dockerfile", - "expected-engine.version", - "!bin/**/*.ts", - "!lib/**/*.ts", - "bin/**/*.d.ts", - "lib/**/*.d.ts" + "expected-engine.version" ], "scripts": { "benchmark": "node scripts/benchmark.mjs", diff --git a/packages/loopover-miner/scripts/benchmark.mjs b/packages/loopover-miner/scripts/benchmark.mjs index e77df7bd79..0a7e3689fc 100644 --- a/packages/loopover-miner/scripts/benchmark.mjs +++ b/packages/loopover-miner/scripts/benchmark.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { performance } from "node:perf_hooks"; -import { rankCandidateIssues } from "../lib/opportunity-ranker.js"; -import { initPortfolioQueueStore } from "../lib/portfolio-queue.js"; +import { rankCandidateIssues } from "../dist/lib/opportunity-ranker.js"; +import { initPortfolioQueueStore } from "../dist/lib/portfolio-queue.js"; // Committed micro-benchmark for the two hot local paths that have no other way to notice a regression: the // discovery fan-out ranking pass (opportunity-ranker.js, run once per repo per discovery cycle over every open diff --git a/packages/loopover-miner/scripts/check-syntax.mjs b/packages/loopover-miner/scripts/check-syntax.mjs index d922df25bf..94ff752cc2 100644 --- a/packages/loopover-miner/scripts/check-syntax.mjs +++ b/packages/loopover-miner/scripts/check-syntax.mjs @@ -17,7 +17,7 @@ function listJsFiles(dir) { .map((entry) => join(dir, entry.name)); } -const files = [...listJsFiles("bin"), ...listJsFiles("lib")].sort(); +const files = [...listJsFiles("dist/bin"), ...listJsFiles("dist/lib")].sort(); const failures = []; for (const file of files) { @@ -36,4 +36,4 @@ if (failures.length > 0) { process.exit(1); } -console.log(`node --check passed for all ${files.length} files in bin/ and lib/.`); +console.log(`node --check passed for all ${files.length} files in dist/bin/ and dist/lib/.`); diff --git a/packages/loopover-miner/scripts/cross-repo-evaluation.mjs b/packages/loopover-miner/scripts/cross-repo-evaluation.mjs index bfe8e4c4d5..42285ef3d7 100644 --- a/packages/loopover-miner/scripts/cross-repo-evaluation.mjs +++ b/packages/loopover-miner/scripts/cross-repo-evaluation.mjs @@ -9,7 +9,7 @@ import { runCrossRepoEvaluation, runCrossRepoFullExecution, summarizeCrossRepoEvaluation, -} from "../lib/cross-repo-evaluation.js"; +} from "../dist/lib/cross-repo-evaluation.js"; const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/packages/loopover-miner/scripts/egress-firewall-entrypoint.sh b/packages/loopover-miner/scripts/egress-firewall-entrypoint.sh index e359bbdf73..2b9f1c4bfa 100644 --- a/packages/loopover-miner/scripts/egress-firewall-entrypoint.sh +++ b/packages/loopover-miner/scripts/egress-firewall-entrypoint.sh @@ -19,7 +19,7 @@ set -eu DNSMASQ_CONF=/etc/dnsmasq.d/loopover-egress.conf RULESET_SCRIPT=/tmp/loopover-egress-ruleset.sh -node /app/packages/loopover-miner/lib/generate-egress-firewall-config.js "$DNSMASQ_CONF" "$RULESET_SCRIPT" +node /app/packages/loopover-miner/dist/lib/generate-egress-firewall-config.js "$DNSMASQ_CONF" "$RULESET_SCRIPT" # Listens on 127.0.0.1 only (per the generated config's own bind-interfaces + listen-address) -- unreachable # from outside this container regardless of what else is on its network. --user/--group are explicit rather diff --git a/packages/loopover-miner/tsconfig.json b/packages/loopover-miner/tsconfig.json index fd26014e29..33e599b601 100644 --- a/packages/loopover-miner/tsconfig.json +++ b/packages/loopover-miner/tsconfig.json @@ -10,22 +10,23 @@ // v8 coverage provider still remaps through it fine, attributing coverage to the .ts source. "inlineSourceMap": true, "noEmit": false, - // Overrides the root config's "dist" so each converted file compiles in place next to its .ts - // source (e.g. lib/foo.ts -> lib/foo.js) -- the package's published bin/lib layout never changes - // as more files convert, so no consumer (in this repo or published) ever needs a different import - // path depending on a given file's migration status. + // Out-of-place emit, matching packages/loopover-engine's src/ -> dist/ split: lib/foo.ts -> dist/lib/foo.js, + // bin/foo.ts -> dist/bin/foo.js. Was in-place (outDir ".") until the 2026-07-24 migration -- a compiled + // .js sitting next to its .ts source in the same directory Vite/vitest also reads caused a real + // codecov/patch (0%/99%) incident (Vite resolved the literal .js over the .ts sibling, misattributing + // coverage and breaking --changed's import-graph tracing) and required a resolver-plugin workaround + // before this proper fix landed. rootDir "." (not "bin"/"lib") preserves the bin/ vs lib/ nesting inside + // dist/, so internal relative imports (e.g. bin/foo.ts's "../lib/bar.js") keep resolving correctly. "rootDir": ".", - "outDir": ".", + "outDir": "dist", // Without this, the inherited root value ("./.tsbuildinfo") resolves relative to the ROOT config's // location, not this one -- both packages would then read/write the exact same cache file at the // repo root and corrupt each other's incremental state. "tsBuildInfoFile": "./.tsbuildinfo" }, - // Every bin/lib runtime module is real TypeScript (#7290 / #7317): tsc owns the in-place .js + .d.ts - // emit, which is gitignored -- contributors and tests never touch it (see .gitignore's comment on - // these paths). The glob stays the include surface as the package's only source of truth. + // Every bin/lib runtime module is real TypeScript (#7290 / #7317): tsc owns the dist/ .js + .d.ts emit, + // which is gitignored -- contributors and tests never touch it. The glob stays the include surface as + // the package's only source of truth. "include": ["bin/**/*.ts", "lib/**/*.ts"], - // Without this, tsc's default exclude list (which always adds outDir) resolves to "." -- the whole - // package root -- and silently excludes every include match, since outDir is "." for in-place emit. "exclude": [] } diff --git a/scripts/check-miner-deployment-docs.ts b/scripts/check-miner-deployment-docs.ts index 92a8af5715..1181601621 100644 --- a/scripts/check-miner-deployment-docs.ts +++ b/scripts/check-miner-deployment-docs.ts @@ -17,9 +17,13 @@ import { 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"); +// Scans the compiled dist/ output (2026-07-24 migration; see tsconfig.json's outDir comment) -- +// this audit needs a prior `npm run build:miner`, same precondition as everything else that reads +// real compiled artifacts (ci.yml's own "MCP/Miner package check" steps already run after "Build +// miner CLI"). +const BIN_DIR = resolve(MINER_DIR, "dist/bin"); const BIN_ENTRY = resolve(BIN_DIR, "loopover-miner.js"); -const LIB_DIR = resolve(MINER_DIR, "lib"); +const LIB_DIR = resolve(MINER_DIR, "dist/lib"); const ENGINE_MINER_DIR = resolve(REPO_ROOT, "packages/loopover-engine/src/miner"); function readFilesWithExtension(dir: string, extension: string): string[] { diff --git a/scripts/check-miner-package.ts b/scripts/check-miner-package.ts index 75b9be92b1..70241630a4 100644 --- a/scripts/check-miner-package.ts +++ b/scripts/check-miner-package.ts @@ -6,12 +6,19 @@ import { fileURLToPath } from "node:url"; import { FORBIDDEN_CONTENT } from "./forbidden-content.js"; const ALLOWED = [ - /^bin\/loopover-miner\.(js|d\.ts)$/, - /^bin\/loopover-miner-mcp\.(js|d\.ts)$/, - // Hosted-container entry point (#7182) -- not REQUIRED like bin/loopover-miner.js, matching + /^dist\/bin\/loopover-miner\.(js|d\.ts)$/, + /^dist\/bin\/loopover-miner-mcp\.(js|d\.ts)$/, + // Hosted-container entry point (#7182) -- not REQUIRED like dist/bin/loopover-miner.js, matching // loopover-miner-mcp's own treatment above (an additional bin, not the package's primary CLI). - /^bin\/loopover-miner-hosted\.(js|d\.ts)$/, - /^lib\/[a-z0-9-]+\.(js|d\.ts)$/, + /^dist\/bin\/loopover-miner-hosted\.(js|d\.ts)$/, + /^dist\/lib\/[a-z0-9-]+\.(js|d\.ts)$/, + // tsc itself (declaration: true + outDir "dist" separate from rootDir, confirmed via the real + // typescript@5.9.3 binary, not an npm/npx artifact) deterministically emits a package.json into the + // output root alongside the .d.ts files -- a module-boundary marker for anyone resolving types/module + // kind directly from dist/ without walking up to the real one. Not REQUIRED (an implementation detail + // of the declaration emission, not something this package's own functionality depends on), but expected + // and allowed rather than flagged as an unexpected file. + /^dist\/package\.json$/, /^package\.json$/, /^README\.md$/, /^expected-engine\.version$/, @@ -22,7 +29,7 @@ const ALLOWED = [ /^schema\/[a-z0-9.-]+\.json$/, ]; const REQUIRED = [ - "bin/loopover-miner.js", + "dist/bin/loopover-miner.js", "package.json", // The operational files #4874 shipped — asserted present so they can never silently drop out of the package again. "DEPLOYMENT.md", @@ -64,8 +71,8 @@ export function validateMinerPackFileList(files: readonly PackedFile[], readCont for (const required of REQUIRED) { if (!paths.includes(required)) throw new Error(`Miner package is missing required file: ${required}`); } - if (!paths.some((file) => /^lib\/([a-z0-9-]+\/)?[a-z0-9-]+\.js$/.test(file))) { - throw new Error("Miner package is missing lib/*.js artifacts"); + if (!paths.some((file) => /^dist\/lib\/([a-z0-9-]+\/)?[a-z0-9-]+\.js$/.test(file))) { + throw new Error("Miner package is missing dist/lib/*.js artifacts"); } if (!paths.some((file) => /^docs\/[a-z0-9-]+\.md$/.test(file))) { throw new Error("Miner package is missing docs/*.md operational documentation"); diff --git a/scripts/mcp-package-allowlist.ts b/scripts/mcp-package-allowlist.ts index fe95c4009f..da2fbedd3f 100644 --- a/scripts/mcp-package-allowlist.ts +++ b/scripts/mcp-package-allowlist.ts @@ -3,12 +3,12 @@ // cannot drift (the previous duplicated lists already missed shipped lib/*.js files). export const MCP_PACKAGE_ALLOWED_FILE_PATTERNS: RegExp[] = [ - /^bin\/loopover-mcp\.js$/, - /^lib\/cli-error\.js$/, - /^lib\/local-branch\.js$/, - /^lib\/format-table\.js$/, - /^lib\/redact-local-path\.js$/, - /^lib\/telemetry\.js$/, + /^dist\/bin\/loopover-mcp\.js$/, + /^dist\/lib\/cli-error\.js$/, + /^dist\/lib\/local-branch\.js$/, + /^dist\/lib\/format-table\.js$/, + /^dist\/lib\/redact-local-path\.js$/, + /^dist\/lib\/telemetry\.js$/, /^scripts\/gittensor-score-preview\.(mjs|py)$/, /^package\.json$/, /^README\.md$/, diff --git a/test/fixtures/miner-concurrent-stores/claim-and-hold-child.mjs b/test/fixtures/miner-concurrent-stores/claim-and-hold-child.mjs index 95c58d3d99..d4bc1f7e00 100644 --- a/test/fixtures/miner-concurrent-stores/claim-and-hold-child.mjs +++ b/test/fixtures/miner-concurrent-stores/claim-and-hold-child.mjs @@ -4,7 +4,7 @@ // claimed entry, then idles forever without ever marking it done -- simulating a process that claims work // and then crashes mid-attempt. The test kills this process (SIGKILL) and asserts the item is left // genuinely stuck 'in_progress' until swept, then reclaimable. -import { initPortfolioQueueStore } from "../../../packages/loopover-miner/lib/portfolio-queue.js"; +import { initPortfolioQueueStore } from "../../../packages/loopover-miner/dist/lib/portfolio-queue.js"; const [dbPath] = process.argv.slice(2); if (!dbPath) { diff --git a/test/fixtures/miner-concurrent-stores/claim-child.mjs b/test/fixtures/miner-concurrent-stores/claim-child.mjs index eebac7596a..eb339dca1e 100644 --- a/test/fixtures/miner-concurrent-stores/claim-child.mjs +++ b/test/fixtures/miner-concurrent-stores/claim-child.mjs @@ -2,7 +2,7 @@ // Cross-process helper for claim-ledger concurrent-race tests (#4867). // Opens the shared ledger, waits for a stdin "go" signal, then calls claimIssue() so multiple Node // processes contend on the same UNIQUE(repo_full_name, issue_number) row via the same dbPath. -import { openClaimLedger } from "../../../packages/loopover-miner/lib/claim-ledger.js"; +import { openClaimLedger } from "../../../packages/loopover-miner/dist/lib/claim-ledger.js"; const [dbPath, repoFullName, issueNumberStr, note] = process.argv.slice(2); if (!dbPath || !repoFullName || !issueNumberStr) { diff --git a/test/fixtures/miner-concurrent-stores/dequeue-child.mjs b/test/fixtures/miner-concurrent-stores/dequeue-child.mjs index 60d22288c0..4aecd34c34 100644 --- a/test/fixtures/miner-concurrent-stores/dequeue-child.mjs +++ b/test/fixtures/miner-concurrent-stores/dequeue-child.mjs @@ -2,7 +2,7 @@ // Cross-process helper for portfolio-queue concurrent-race tests (#4867). // Opens the shared queue, waits for a stdin "go" signal, then calls dequeueNext() so multiple Node // processes contend on the same atomic UPDATE...RETURNING claim via the same dbPath. -import { initPortfolioQueueStore } from "../../../packages/loopover-miner/lib/portfolio-queue.js"; +import { initPortfolioQueueStore } from "../../../packages/loopover-miner/dist/lib/portfolio-queue.js"; const [dbPath] = process.argv.slice(2); if (!dbPath) { diff --git a/test/fixtures/miner-worktree-allocator/acquire-child.mjs b/test/fixtures/miner-worktree-allocator/acquire-child.mjs index cb2bc8c2d9..955f510e97 100644 --- a/test/fixtures/miner-worktree-allocator/acquire-child.mjs +++ b/test/fixtures/miner-worktree-allocator/acquire-child.mjs @@ -2,7 +2,7 @@ // Cross-process helper for worktree-allocator collision tests (#4298). // Opens the shared store, waits for a stdin "go" signal, then calls acquire() so // multiple Node processes contend on BEGIN IMMEDIATE against the same dbPath. -import { openWorktreeAllocator } from "../../../packages/loopover-miner/lib/worktree-allocator.js"; +import { openWorktreeAllocator } from "../../../packages/loopover-miner/dist/lib/worktree-allocator.js"; const [dbPath, worktreeBaseDir, maxConcurrencyStr, attemptId, repoFullName] = process.argv.slice(2); if (!dbPath || !worktreeBaseDir || !maxConcurrencyStr || !attemptId || !repoFullName) { diff --git a/test/unit/check-mcp-package.test.ts b/test/unit/check-mcp-package.test.ts index 512957254e..9b9c8abe79 100644 --- a/test/unit/check-mcp-package.test.ts +++ b/test/unit/check-mcp-package.test.ts @@ -25,9 +25,9 @@ const FULL_PACKAGE = [ "README.md", "CHANGELOG.md", "LICENSE", - "bin/loopover-mcp.js", - "lib/cli-error.js", - "lib/telemetry.js", + "dist/bin/loopover-mcp.js", + "dist/lib/cli-error.js", + "dist/lib/telemetry.js", "scripts/gittensor-score-preview.mjs", "scripts/gittensor-score-preview.py", ]; @@ -37,7 +37,7 @@ describe("check-mcp-package script", () => { const result = runChecker(); expect(result.status).toBe(0); expect(result.out).toMatch(/^MCP package dry-run ok:/); - expect(result.out).toContain("bin/loopover-mcp.js"); + expect(result.out).toContain("dist/bin/loopover-mcp.js"); expect(result.out).toContain("package.json"); }); @@ -48,7 +48,7 @@ describe("check-mcp-package script", () => { }); expect(result.status).toBe(0); expect(result.out).toMatch(/^MCP package dry-run ok:/); - expect(result.out).toContain("lib/cli-error.js"); + expect(result.out).toContain("dist/lib/cli-error.js"); expect(result.out).toContain("scripts/gittensor-score-preview.mjs"); }); @@ -66,17 +66,17 @@ describe("check-mcp-package script", () => { it("rejects an unexpected bin that matches the package name prefix", () => { const result = runChecker({ - CHECK_MCP_PACK_TEST_FILES: JSON.stringify(["package.json", "bin/loopover-mcp-backdoor.js"]), + CHECK_MCP_PACK_TEST_FILES: JSON.stringify(["package.json", "dist/bin/loopover-mcp-backdoor.js"]), CHECK_MCP_PACK_TEST_CONTENT: "console.log('not secret');", }); expect(result.status).toBe(1); - expect(result.out).toContain("Unexpected file in MCP package: bin/loopover-mcp-backdoor.js"); + expect(result.out).toContain("Unexpected file in MCP package: dist/bin/loopover-mcp-backdoor.js"); }); it("rejects secret-like content", () => { const probe = ["PROBE", "_", "SECRET", "=", "value"].join(""); const result = runChecker({ - CHECK_MCP_PACK_TEST_FILES: JSON.stringify(["package.json", "bin/loopover-mcp.js"]), + CHECK_MCP_PACK_TEST_FILES: JSON.stringify(["package.json", "dist/bin/loopover-mcp.js"]), CHECK_MCP_PACK_TEST_CONTENT: probe, }); expect(result.status).toBe(1); diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts index 30f68d1ba7..d3cb488f36 100644 --- a/test/unit/check-miner-package.test.ts +++ b/test/unit/check-miner-package.test.ts @@ -24,7 +24,7 @@ describe("check-miner-package script", () => { const result = runChecker(); expect(result.status).toBe(0); expect(result.out).toMatch(/^Miner package dry-run ok:/); - expect(result.out).toContain("bin/loopover-miner.js"); + expect(result.out).toContain("dist/bin/loopover-miner.js"); expect(result.out).toContain("package.json"); }); @@ -39,10 +39,10 @@ describe("check-miner-package script", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", - "lib/secret-helper.js", - "lib/secret-helper.d.ts", - "lib/private-key-rotation.js", + "dist/bin/loopover-miner.js", + "dist/lib/secret-helper.js", + "dist/lib/secret-helper.d.ts", + "dist/lib/private-key-rotation.js", "DEPLOYMENT.md", "Dockerfile", "docs/coding-agent-driver.md", @@ -52,8 +52,8 @@ describe("check-miner-package script", () => { }); expect(result.status).toBe(0); expect(result.out).toMatch(/^Miner package dry-run ok:/); - expect(result.out).toContain("lib/secret-helper.js"); - expect(result.out).toContain("lib/private-key-rotation.js"); + expect(result.out).toContain("dist/lib/secret-helper.js"); + expect(result.out).toContain("dist/lib/private-key-rotation.js"); }); it("still rejects a non-code file whose name suggests it IS a secret (.pem, secrets.json)", () => { @@ -79,11 +79,11 @@ describe("check-miner-package script", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", + "dist/bin/loopover-miner.js", "README.md", "DEPLOYMENT.md", "Dockerfile", - "lib/cli.js", + "dist/lib/cli.js", "docs/quickstart.md", "schema/miner-goal-spec.schema.json", ]), @@ -103,48 +103,48 @@ describe("check-miner-package script", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", - "bin/loopover-miner-backdoor.js", - "lib/cli.js", + "dist/bin/loopover-miner.js", + "dist/bin/loopover-miner-backdoor.js", + "dist/lib/cli.js", ]), CHECK_MINER_PACK_TEST_CONTENT: "console.log('not secret');", }); expect(result.status).toBe(1); - expect(result.out).toContain("Unexpected file in miner package: bin/loopover-miner-backdoor.js"); + expect(result.out).toContain("Unexpected file in miner package: dist/bin/loopover-miner-backdoor.js"); }); it("REGRESSION (#3704 caused main to go red, fixed by flattening lib/ instead of widening this allowlist): rejects a lib module nested one level under a subdirectory", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", - "lib/cli.js", - "lib/calibration/index.js", + "dist/bin/loopover-miner.js", + "dist/lib/cli.js", + "dist/lib/calibration/index.js", ]), }); expect(result.status).toBe(1); - expect(result.out).toContain("Unexpected file in miner package: lib/calibration/index.js"); + expect(result.out).toContain("Unexpected file in miner package: dist/lib/calibration/index.js"); }); it("rejects a file nested two levels deep under lib/", () => { const result = runChecker({ CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", - "lib/cli.js", - "lib/calibration/nested/index.js", + "dist/bin/loopover-miner.js", + "dist/lib/cli.js", + "dist/lib/calibration/nested/index.js", ]), }); expect(result.status).toBe(1); - expect(result.out).toContain("Unexpected file in miner package: lib/calibration/nested/index.js"); + expect(result.out).toContain("Unexpected file in miner package: dist/lib/calibration/nested/index.js"); }); it("rejects a package missing the CLI bin", () => { const result = runChecker({ - CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json", "lib/cli.js"]), + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json", "dist/lib/cli.js"]), }); expect(result.status).toBe(1); - expect(result.out).toContain("Miner package is missing required file: bin/loopover-miner.js"); + expect(result.out).toContain("Miner package is missing required file: dist/bin/loopover-miner.js"); }); it("rejects a package missing lib artifacts", () => { @@ -152,7 +152,7 @@ describe("check-miner-package script", () => { // Every REQUIRED file present so the check reaches (and fails on) the lib-artifacts guard specifically. CHECK_MINER_PACK_TEST_FILES: JSON.stringify([ "package.json", - "bin/loopover-miner.js", + "dist/bin/loopover-miner.js", "DEPLOYMENT.md", "Dockerfile", "schema/miner-goal-spec.schema.json", @@ -160,13 +160,13 @@ describe("check-miner-package script", () => { CHECK_MINER_PACK_TEST_CONTENT: "{}", }); expect(result.status).toBe(1); - expect(result.out).toContain("Miner package is missing lib/*.js artifacts"); + expect(result.out).toContain("Miner package is missing dist/lib/*.js artifacts"); }); it("rejects secret-like content", () => { const probe = ["PROBE", "_", "SECRET", "=", "value"].join(""); const result = runChecker({ - CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json", "bin/loopover-miner.js", "lib/cli.js"]), + CHECK_MINER_PACK_TEST_FILES: JSON.stringify(["package.json", "dist/bin/loopover-miner.js", "dist/lib/cli.js"]), CHECK_MINER_PACK_TEST_CONTENT: probe, }); expect(result.status).toBe(1); @@ -178,8 +178,8 @@ describe("check-miner-package script", () => { // and the schema — is accepted. const FULL_PACKAGE = [ "package.json", - "bin/loopover-miner.js", - "lib/cli.js", + "dist/bin/loopover-miner.js", + "dist/lib/cli.js", "DEPLOYMENT.md", "Dockerfile", "docs/coding-agent-driver.md", diff --git a/test/unit/forbidden-content.test.ts b/test/unit/forbidden-content.test.ts index 47b4e1f6c1..431bf61699 100644 --- a/test/unit/forbidden-content.test.ts +++ b/test/unit/forbidden-content.test.ts @@ -14,8 +14,8 @@ const PACKAGE_CHECKERS = ["scripts/check-miner-package.ts", "scripts/check-mcp-p // A minimal file list that passes each checker's path/allowlist/required-file guards, so the run reaches the // shared secret-content read. Mirrors the file lists each checker's own "rejects secret-like content" test uses. const REACHABLE_FILES: Record = { - "scripts/check-miner-package.ts": ["package.json", "bin/loopover-miner.js", "lib/cli.js"], - "scripts/check-mcp-package.ts": ["package.json", "bin/loopover-mcp.js"], + "scripts/check-miner-package.ts": ["package.json", "dist/bin/loopover-miner.js", "dist/lib/cli.js"], + "scripts/check-mcp-package.ts": ["package.json", "dist/bin/loopover-mcp.js"], }; // Assembled from fragments so this file never itself contains a credential-shaped literal -- the same diff --git a/test/unit/mcp-cli-boundary-tests-tool.test.ts b/test/unit/mcp-cli-boundary-tests-tool.test.ts index 01642d80c9..1d473bc432 100644 --- a/test/unit/mcp-cli-boundary-tests-tool.test.ts +++ b/test/unit/mcp-cli-boundary-tests-tool.test.ts @@ -11,7 +11,7 @@ import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harnes // (src/signals/boundary-test-generation.ts depends on the app's AdvisoryFinding type), not in @loopover/engine. // The route therefore stays the single source of truth; these tests pin the request the tool composes and its // zod shape, and routes-boundary-tests.test.ts pins the verdict itself. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; let configDir: string; diff --git a/test/unit/mcp-cli-bounty-advisory.test.ts b/test/unit/mcp-cli-bounty-advisory.test.ts index 820d7b3d6d..f1d2fa8b57 100644 --- a/test/unit/mcp-cli-bounty-advisory.test.ts +++ b/test/unit/mcp-cli-bounty-advisory.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-burden-forecast.test.ts b/test/unit/mcp-cli-burden-forecast.test.ts index bb28678f7c..51307e28b0 100644 --- a/test/unit/mcp-cli-burden-forecast.test.ts +++ b/test/unit/mcp-cli-burden-forecast.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-completion-spec.test.ts b/test/unit/mcp-cli-completion-spec.test.ts index e5ae448a5c..b46c1b5113 100644 --- a/test/unit/mcp-cli-completion-spec.test.ts +++ b/test/unit/mcp-cli-completion-spec.test.ts @@ -13,7 +13,7 @@ import { run } from "./support/mcp-cli-harness"; // asserts the INVARIANT instead: every canonical subcommand a run*Cli really accepts must appear in the spec. // The source is parsed rather than imported because bin/loopover-mcp.js is an executable entrypoint that starts // a server on import — reading it is how a test can inspect the spec without launching one. -const SOURCE = readFileSync(join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"), "utf8"); +const SOURCE = readFileSync(join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"), "utf8"); /** The declared spec, read out of the committed source. */ function declaredSpec(): Record { diff --git a/test/unit/mcp-cli-draft-pr-body.test.ts b/test/unit/mcp-cli-draft-pr-body.test.ts index b4031759aa..73d89c3e51 100644 --- a/test/unit/mcp-cli-draft-pr-body.test.ts +++ b/test/unit/mcp-cli-draft-pr-body.test.ts @@ -13,7 +13,7 @@ import { } from "./support/mcp-cli-harness"; // #6741: CLI stdio mirror of loopover_draft_pr_body — analyzeCurrentBranch then local buildPublicPrBodyDraft. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; diff --git a/test/unit/mcp-cli-evaluate-escalation-tool.test.ts b/test/unit/mcp-cli-evaluate-escalation-tool.test.ts index 6e308db9e5..0f2baa68d4 100644 --- a/test/unit/mcp-cli-evaluate-escalation-tool.test.ts +++ b/test/unit/mcp-cli-evaluate-escalation-tool.test.ts @@ -10,7 +10,7 @@ import { evaluateEscalation } from "../../src/loop-escalation"; // it computes IN-PROCESS from @loopover/engine — no API round-trip — so escalation checks work fully offline. // The point of these tests is cross-surface PARITY: the stdio tool must return exactly what the pure // evaluateEscalation returns for identical input (the same function /v1/loop/evaluate-escalation delegates to). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-explain-gate-disposition.test.ts b/test/unit/mcp-cli-explain-gate-disposition.test.ts index 0706136cac..d2153efa00 100644 --- a/test/unit/mcp-cli-explain-gate-disposition.test.ts +++ b/test/unit/mcp-cli-explain-gate-disposition.test.ts @@ -14,7 +14,7 @@ import { // #6740: CLI stdio mirror of loopover_explain_gate_disposition — fetches predictedGate via the same // /v1/local/branch-analysis route as loopover_predict_gate, then runs the shared buildGateDispositions // locally so MCP and CLI agree by construction. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; diff --git a/test/unit/mcp-cli-find-opportunities.test.ts b/test/unit/mcp-cli-find-opportunities.test.ts index e3d63c24ef..f7e889b736 100644 --- a/test/unit/mcp-cli-find-opportunities.test.ts +++ b/test/unit/mcp-cli-find-opportunities.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-help.test.ts b/test/unit/mcp-cli-help.test.ts index 7d3b120ff4..b7c7058aa6 100644 --- a/test/unit/mcp-cli-help.test.ts +++ b/test/unit/mcp-cli-help.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { run } from "./support/mcp-cli-harness"; // #6991: printHelp() listed ~30 real top-level commands but omitted two dispatched ones: `maintain` -// (packages/loopover-mcp/bin/loopover-mcp.js's maintainCli) and `contributor-profile` +// (packages/loopover-mcp/dist/bin/loopover-mcp.js's maintainCli) and `contributor-profile` // (contributorProfileCli, added by #6737). A user running `loopover-mcp --help` had no way to // discover either command exists. describe("loopover-mcp --help lists every real top-level command (#6991)", () => { diff --git a/test/unit/mcp-cli-intake-idea-tool.test.ts b/test/unit/mcp-cli-intake-idea-tool.test.ts index 64f9892c0a..8a962438cd 100644 --- a/test/unit/mcp-cli-intake-idea-tool.test.ts +++ b/test/unit/mcp-cli-intake-idea-tool.test.ts @@ -10,7 +10,7 @@ import { buildTaskGraph, validateIdeaSubmission } from "../../src/idea-intake"; // computes IN-PROCESS from @loopover/engine — no API round-trip — so idea intake works fully offline. The point // of these tests is cross-surface PARITY: the stdio tool must return exactly what the pure bridge returns for // identical input (the same functions /v1/loop/intake-idea delegates to), including the actionable error list. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-intake-tools.test.ts b/test/unit/mcp-cli-intake-tools.test.ts index ed6daf03d5..647e82d0a6 100644 --- a/test/unit/mcp-cli-intake-tools.test.ts +++ b/test/unit/mcp-cli-intake-tools.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); // #6151: the maintainer-triage / repo-owner-intake profiles recommend these 4 tools; assert none leak // miner-private reward internals through the local stdio proxy. const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; diff --git a/test/unit/mcp-cli-issue-rag.test.ts b/test/unit/mcp-cli-issue-rag.test.ts index bd3af5d579..dc3e7598ef 100644 --- a/test/unit/mcp-cli-issue-rag.test.ts +++ b/test/unit/mcp-cli-issue-rag.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-label-audit.test.ts b/test/unit/mcp-cli-label-audit.test.ts index 43fbba430d..75a88eed03 100644 --- a/test/unit/mcp-cli-label-audit.test.ts +++ b/test/unit/mcp-cli-label-audit.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-maintain-tools.test.ts b/test/unit/mcp-cli-maintain-tools.test.ts index edf86a7333..ba693f5c6e 100644 --- a/test/unit/mcp-cli-maintain-tools.test.ts +++ b/test/unit/mcp-cli-maintain-tools.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); // #6152: the maintain CLI's REST surface, exposed as stdio tools. These assert the proxy contract -- that each // tool reaches the endpoint its CLI subcommand already calls, with the same method and body -- rather than diff --git a/test/unit/mcp-cli-maintain.test.ts b/test/unit/mcp-cli-maintain.test.ts index a9b3656415..b524491a21 100644 --- a/test/unit/mcp-cli-maintain.test.ts +++ b/test/unit/mcp-cli-maintain.test.ts @@ -9,7 +9,7 @@ import { closeFixtureServer, repoOnboardingPackFixture, runAsync, startFixtureSe // through its published export map, which doesn't surface AUTONOMY_LEVELS), so nothing but a test can catch the // two drifting apart. The source is parsed rather than imported because bin/loopover-mcp.js is an executable // entrypoint that starts a server on import. -const CLI_SOURCE = readFileSync(join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"), "utf8"); +const CLI_SOURCE = readFileSync(join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"), "utf8"); /** The `maintain set-level` levels the committed CLI source really accepts. */ function declaredLevels(): string[] { diff --git a/test/unit/mcp-cli-maintainer-lane.test.ts b/test/unit/mcp-cli-maintainer-lane.test.ts index 69b8ac4e8e..165c579aa4 100644 --- a/test/unit/mcp-cli-maintainer-lane.test.ts +++ b/test/unit/mcp-cli-maintainer-lane.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-maintainer-noise.test.ts b/test/unit/mcp-cli-maintainer-noise.test.ts index 4a71340206..c475faa1f8 100644 --- a/test/unit/mcp-cli-maintainer-noise.test.ts +++ b/test/unit/mcp-cli-maintainer-noise.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-monitor-open-prs.test.ts b/test/unit/mcp-cli-monitor-open-prs.test.ts index b6cb511451..d424619785 100644 --- a/test/unit/mcp-cli-monitor-open-prs.test.ts +++ b/test/unit/mcp-cli-monitor-open-prs.test.ts @@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; // so run()'s execFileSync would block the event loop and the child's fetch would abort before a response. import { closeFixtureServer, openPrMonitorFixture, run, runAsync, runExpectingFailure, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-open-pr-pressure-tool.test.ts b/test/unit/mcp-cli-open-pr-pressure-tool.test.ts index 5c6b3348a4..970931f385 100644 --- a/test/unit/mcp-cli-open-pr-pressure-tool.test.ts +++ b/test/unit/mcp-cli-open-pr-pressure-tool.test.ts @@ -11,7 +11,7 @@ import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harnes // source of truth for the ranking. The bin cannot import from src/, so its zod shape is a hand-mirror of the // tool's — these tests pin that mirror: a payload the shape accepts reaches the route, and the fields the real // schema requires (nested signals, findings) are enforced here too rather than being waved through. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; let configDir: string; diff --git a/test/unit/mcp-cli-plan-idea-claims-tool.test.ts b/test/unit/mcp-cli-plan-idea-claims-tool.test.ts index 456655bda1..6c61d7a3b7 100644 --- a/test/unit/mcp-cli-plan-idea-claims-tool.test.ts +++ b/test/unit/mcp-cli-plan-idea-claims-tool.test.ts @@ -10,7 +10,7 @@ import { buildClaimPlan, buildTaskGraph, validateIdeaSubmission } from "../../sr // it computes IN-PROCESS from @loopover/engine — no API round-trip — so claim planning works fully offline. // Cross-surface PARITY: the stdio tool must return exactly what the pure handler returns for identical input // (the same functions /v1/loop/plan-idea-claims delegates to). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-plan-scorer-tools.test.ts b/test/unit/mcp-cli-plan-scorer-tools.test.ts index 61844ae380..50836bb6ee 100644 --- a/test/unit/mcp-cli-plan-scorer-tools.test.ts +++ b/test/unit/mcp-cli-plan-scorer-tools.test.ts @@ -10,7 +10,7 @@ import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harnes // recommendedTools but never actually registered on the local stdio server. These tests drive the real // stdio server and assert each tool's composed output, plus a zod-rejection failure path per pure tool // and an API-failure path for the one HTTP-backed tool (loopover_predict_gate). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const PLAN_SCORER_TOOLS = ["loopover_run_local_scorer", "loopover_build_plan", "loopover_plan_status", "loopover_record_step_result", "loopover_predict_gate"]; diff --git a/test/unit/mcp-cli-pr-ai-review-findings.test.ts b/test/unit/mcp-cli-pr-ai-review-findings.test.ts index 7bb06fc636..290ba180fa 100644 --- a/test/unit/mcp-cli-pr-ai-review-findings.test.ts +++ b/test/unit/mcp-cli-pr-ai-review-findings.test.ts @@ -10,7 +10,7 @@ import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harnes // author login and proxies to GET /v1/repos/:owner/:repo/pulls/:number/ai-review-findings — the route (and the // loadPrAiReviewFindings it delegates to) stays the single source of truth, so these tests assert the request // the tool composes (path + login query param) and its login-resolution fallback chain, not the findings logic. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-pr-outcomes.test.ts b/test/unit/mcp-cli-pr-outcomes.test.ts index de2ed42a67..2ea2579aeb 100644 --- a/test/unit/mcp-cli-pr-outcomes.test.ts +++ b/test/unit/mcp-cli-pr-outcomes.test.ts @@ -8,7 +8,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, prOutcomesFixture, run, runAsync, runExpectingFailure, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-pr-reviewability.test.ts b/test/unit/mcp-cli-pr-reviewability.test.ts index 54939381e9..c654e95563 100644 --- a/test/unit/mcp-cli-pr-reviewability.test.ts +++ b/test/unit/mcp-cli-pr-reviewability.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-progress-snapshot-tool.test.ts b/test/unit/mcp-cli-progress-snapshot-tool.test.ts index 5055eac3f4..76b03e237d 100644 --- a/test/unit/mcp-cli-progress-snapshot-tool.test.ts +++ b/test/unit/mcp-cli-progress-snapshot-tool.test.ts @@ -11,7 +11,7 @@ import { buildProgressSnapshot, type LoopProgressState } from "../../src/loop-pr // progress composition works fully offline. The point of these tests is cross-surface PARITY: the // stdio tool must return exactly what the pure buildProgressSnapshot returns for identical input // (the same function /v1/loop/progress-snapshot delegates to). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-repo-outcome-patterns.test.ts b/test/unit/mcp-cli-repo-outcome-patterns.test.ts index fb89016f72..d1380d2bc4 100644 --- a/test/unit/mcp-cli-repo-outcome-patterns.test.ts +++ b/test/unit/mcp-cli-repo-outcome-patterns.test.ts @@ -12,7 +12,7 @@ import { // #6734: CLI stdio mirror of loopover_get_repo_outcome_patterns — thin GET proxy of the public // /v1/repos/:owner/:repo/outcome-patterns route (same ownerRepoShape + apiGet pattern as maintainer_noise). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; diff --git a/test/unit/mcp-cli-results-payload-tool.test.ts b/test/unit/mcp-cli-results-payload-tool.test.ts index 2c5a6e7661..23757ff9af 100644 --- a/test/unit/mcp-cli-results-payload-tool.test.ts +++ b/test/unit/mcp-cli-results-payload-tool.test.ts @@ -10,7 +10,7 @@ import { buildResultsPayload, type IterationResult } from "../../src/results-pay // it composes IN-PROCESS from @loopover/engine — no API round-trip — so results composition works fully offline. // The point of these tests is cross-surface PARITY: the stdio tool must return exactly what the pure // buildResultsPayload returns for identical input (the same function /v1/loop/results-payload delegates to). -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-review-risk.test.ts b/test/unit/mcp-cli-review-risk.test.ts index ffea7df5b0..b1a502cab8 100644 --- a/test/unit/mcp-cli-review-risk.test.ts +++ b/test/unit/mcp-cli-review-risk.test.ts @@ -8,7 +8,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, reviewRiskFixture, run, runAsync, runExpectingFailure, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-test-evidence-tool.test.ts b/test/unit/mcp-cli-test-evidence-tool.test.ts index f3ded94bfd..c7e845bf55 100644 --- a/test/unit/mcp-cli-test-evidence-tool.test.ts +++ b/test/unit/mcp-cli-test-evidence-tool.test.ts @@ -10,7 +10,7 @@ import { buildTestEvidenceReport } from "../../src/signals/test-evidence"; // it computes IN-PROCESS from @loopover/engine — no API round-trip — so coverage self-checks work offline. // These assert cross-surface parity with the same buildTestEvidenceReport the route + MCP tool call; the // builder's own correctness is pinned independently by test-evidence-report.test.ts. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; let configDir: string; diff --git a/test/unit/mcp-cli-tools.test.ts b/test/unit/mcp-cli-tools.test.ts index 6ab21eafb8..0cdc2524aa 100644 --- a/test/unit/mcp-cli-tools.test.ts +++ b/test/unit/mcp-cli-tools.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); describe("loopover-mcp CLI — tools", () => { let configDir: string | null = null; diff --git a/test/unit/mcp-cli-upstream-drift.test.ts b/test/unit/mcp-cli-upstream-drift.test.ts index 6a7d88052d..826c319676 100644 --- a/test/unit/mcp-cli-upstream-drift.test.ts +++ b/test/unit/mcp-cli-upstream-drift.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeFixtureServer, startFixtureServer } from "./support/mcp-cli-harness"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; let client: Client; diff --git a/test/unit/mcp-cli-validate-config-offline.test.ts b/test/unit/mcp-cli-validate-config-offline.test.ts index 0c7b21b6b6..57ce04d2c2 100644 --- a/test/unit/mcp-cli-validate-config-offline.test.ts +++ b/test/unit/mcp-cli-validate-config-offline.test.ts @@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; // builder (buildFocusManifestValidation) instead of POSTing to /v1/validate/focus-manifest. These tests drive // the real local stdio server with a DELIBERATELY UNREACHABLE API URL to prove the tool validates fully offline // -- if it still round-tripped to the API, every call here would fail/time out. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-cli-write-tools.test.ts b/test/unit/mcp-cli-write-tools.test.ts index a152b5b942..697dd57017 100644 --- a/test/unit/mcp-cli-write-tools.test.ts +++ b/test/unit/mcp-cli-write-tools.test.ts @@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; // #6149: the 8 miner write-tools are PURE local-execution spec builders (loopover never performs the write); // each returns a { action, command, boundary } spec the caller runs with its OWN gh/git creds. These tests // drive the real local stdio server and assert the composed spec, plus a zod-rejection failure path per tool. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-discovery.test.ts b/test/unit/mcp-discovery.test.ts index a7bcb48890..c038759879 100644 --- a/test/unit/mcp-discovery.test.ts +++ b/test/unit/mcp-discovery.test.ts @@ -8,7 +8,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PUBLIC_TERMS = /wallet\s*[:=]\s*\S+|hotkey\s*[:=]\s*\S+|coldkey\s*[:=]\s*\S+|raw trust score is|your trust score|reward estimate is|estimated reward/i; diff --git a/test/unit/mcp-feasibility-gate.test.ts b/test/unit/mcp-feasibility-gate.test.ts index 5d453e0648..aff84af0cf 100644 --- a/test/unit/mcp-feasibility-gate.test.ts +++ b/test/unit/mcp-feasibility-gate.test.ts @@ -7,7 +7,7 @@ import { DatabaseSync } from "node:sqlite"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { openClaimLedger } from "../../packages/loopover-miner/lib/claim-ledger.js"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-lint-pr-text.test.ts b/test/unit/mcp-lint-pr-text.test.ts index b5fba6ad2f..fa560d0899 100644 --- a/test/unit/mcp-lint-pr-text.test.ts +++ b/test/unit/mcp-lint-pr-text.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-local-check-slop-risk.test.ts b/test/unit/mcp-local-check-slop-risk.test.ts index dc6b4823e8..4448c9b374 100644 --- a/test/unit/mcp-local-check-slop-risk.test.ts +++ b/test/unit/mcp-local-check-slop-risk.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); let client: Client; let transport: StdioClientTransport; diff --git a/test/unit/mcp-local-telemetry-chokepoint.test.ts b/test/unit/mcp-local-telemetry-chokepoint.test.ts index 8739412b57..27a4b700ec 100644 --- a/test/unit/mcp-local-telemetry-chokepoint.test.ts +++ b/test/unit/mcp-local-telemetry-chokepoint.test.ts @@ -13,7 +13,7 @@ import { afterEach, describe, expect, it } from "vitest"; // real subprocess, where an in-process vi.mock could not reach it anyway. Instead they point the SDK at a local // recorder via LOOPOVER_MCP_POSTHOG_HOST and assert what actually leaves the process. Default-off is therefore // verified, not documented. -const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); type PostHogEvent = { event: string; distinct_id?: string; properties?: Record }; diff --git a/test/unit/mcp-release-candidate.test.ts b/test/unit/mcp-release-candidate.test.ts index 75a2523b33..fc005d0654 100644 --- a/test/unit/mcp-release-candidate.test.ts +++ b/test/unit/mcp-release-candidate.test.ts @@ -18,11 +18,11 @@ const FORBIDDEN_PUBLIC_LANGUAGE = /\b(wallet|hotkey|coldkey|raw trust|trust scor // Mirrors the allowlisted shape of the published tarball (must stay aligned with MCP_PACKAGE_ALLOWED_FILE_PATTERNS). const ALLOWED_FILES = [ - "bin/loopover-mcp.js", - "lib/cli-error.js", - "lib/local-branch.js", - "lib/format-table.js", - "lib/redact-local-path.js", + "dist/bin/loopover-mcp.js", + "dist/lib/cli-error.js", + "dist/lib/local-branch.js", + "dist/lib/format-table.js", + "dist/lib/redact-local-path.js", "scripts/gittensor-score-preview.mjs", "package.json", "README.md", @@ -89,7 +89,7 @@ describe("checkChangelog", () => { describe("checkTarball", () => { it("accepts every shipped MCP lib file previously missing from the RC allowlist (#6291)", () => { - for (const file of ["lib/cli-error.js", "lib/format-table.js", "lib/redact-local-path.js"]) { + for (const file of ["dist/lib/cli-error.js", "dist/lib/format-table.js", "dist/lib/redact-local-path.js"]) { expect(unexpectedTarballFiles([file])).toEqual([]); expect(MCP_PACKAGE_ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file))).toBe(true); } diff --git a/test/unit/miner-attempt-log.test.ts b/test/unit/miner-attempt-log.test.ts index b44f154a32..5a7e9fd1db 100644 --- a/test/unit/miner-attempt-log.test.ts +++ b/test/unit/miner-attempt-log.test.ts @@ -261,7 +261,7 @@ describe("loopover-miner attempt log (#4294)", () => { }); it("is append-only: the module source issues no UPDATE or DELETE against the ledger", () => { - const source = readFileSync("packages/loopover-miner/lib/attempt-log.js", "utf8"); + const source = readFileSync("packages/loopover-miner/dist/lib/attempt-log.js", "utf8"); expect(source).not.toMatch(/\b(UPDATE|DELETE)\b/i); }); diff --git a/test/unit/miner-claim-ledger.test.ts b/test/unit/miner-claim-ledger.test.ts index 1e19f00b24..35f3982ca0 100644 --- a/test/unit/miner-claim-ledger.test.ts +++ b/test/unit/miner-claim-ledger.test.ts @@ -152,7 +152,7 @@ describe("loopover-miner claim ledger (#2314)", () => { }); it("documents that miner_claims is local bookkeeping only, not duplicate adjudication (#3355)", () => { - const source = readFileSync("packages/loopover-miner/lib/claim-ledger.js", "utf8"); + const source = readFileSync("packages/loopover-miner/dist/lib/claim-ledger.js", "utf8"); expect(source).toContain("LOCAL bookkeeping only"); expect(source).toContain("does NOT adjudicate contested duplicates"); expect(source).toContain("isDuplicateClusterWinnerByClaim"); diff --git a/test/unit/miner-deployment-docs-audit.test.ts b/test/unit/miner-deployment-docs-audit.test.ts index d021e9cacb..7a6405fbc4 100644 --- a/test/unit/miner-deployment-docs-audit.test.ts +++ b/test/unit/miner-deployment-docs-audit.test.ts @@ -12,14 +12,17 @@ import { scanEnvVarTokens, scanRegisteredCommands, } from "../../packages/loopover-miner/lib/deployment-docs-audit.js"; -import type { DeploymentDocsReality } from "../../packages/loopover-miner/lib/deployment-docs-audit.d.ts"; +import type { DeploymentDocsReality } from "../../packages/loopover-miner/lib/deployment-docs-audit.js"; const REPO_ROOT = resolve(import.meta.dirname, "../.."); const MINER_DIR = resolve(REPO_ROOT, "packages/loopover-miner"); const DEPLOYMENT_MD = resolve(MINER_DIR, "DEPLOYMENT.md"); -const BIN_DIR = resolve(MINER_DIR, "bin"); +// dist/ (2026-07-24 migration; see tsconfig.json's outDir comment) -- this scan needs a prior +// `npm run build:miner`, same precondition it already had pre-migration (lib/bin's compiled .js was +// always gitignored build output, never committed either way). +const BIN_DIR = resolve(MINER_DIR, "dist/bin"); const BIN_ENTRY = resolve(BIN_DIR, "loopover-miner.js"); -const LIB_DIR = resolve(MINER_DIR, "lib"); +const LIB_DIR = resolve(MINER_DIR, "dist/lib"); // loopover-miner's coding-agent driver construction (MINER_CODING_AGENT_*) is implemented in the // loopover-engine package it depends on, not under packages/loopover-miner/** -- an env var read only // there would otherwise false-positive as undocumented-in-code. Source (not dist/, which is gitignored and diff --git a/test/unit/miner-event-ledger.test.ts b/test/unit/miner-event-ledger.test.ts index 944bfe178f..e0dda78ad0 100644 --- a/test/unit/miner-event-ledger.test.ts +++ b/test/unit/miner-event-ledger.test.ts @@ -197,7 +197,7 @@ describe("loopover-miner event ledger (#2290)", () => { }); it("is append-only: the module's own source issues no inline UPDATE or DELETE against the ledger (#5564: the sole exception, purgeByRepo, delegates its DELETE to store-maintenance.js's shared helper, never inline SQL here)", () => { - const source = readFileSync("packages/loopover-miner/lib/event-ledger.js", "utf8"); + const source = readFileSync("packages/loopover-miner/dist/lib/event-ledger.js", "utf8"); expect(source).not.toMatch(/\b(UPDATE|DELETE)\b/i); expect(source).toContain("purgeByRepo"); }); diff --git a/test/unit/miner-mcp-tool-docs-parity.test.ts b/test/unit/miner-mcp-tool-docs-parity.test.ts index c81ee2c0b8..c281d8c011 100644 --- a/test/unit/miner-mcp-tool-docs-parity.test.ts +++ b/test/unit/miner-mcp-tool-docs-parity.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -const MCP_BIN_PATH = join(process.cwd(), "packages/loopover-miner/bin/loopover-miner-mcp.js"); +const MCP_BIN_PATH = join(process.cwd(), "packages/loopover-miner/dist/bin/loopover-miner-mcp.js"); const README_PATH = join(process.cwd(), "packages/loopover-miner/README.md"); const CODING_AGENT_DRIVER_DOC_PATH = join(process.cwd(), "packages/loopover-miner/docs/coding-agent-driver.md"); diff --git a/test/unit/miner-package-skeleton.test.ts b/test/unit/miner-package-skeleton.test.ts index 8181dd4989..adc2297008 100644 --- a/test/unit/miner-package-skeleton.test.ts +++ b/test/unit/miner-package-skeleton.test.ts @@ -34,17 +34,17 @@ describe("loopover-miner package skeleton (#2287)", () => { expect(miner.license).toBe("AGPL-3.0-only"); expect(miner.type).toBe("module"); expect(miner.bin).toEqual({ - "loopover-miner": "bin/loopover-miner.js", - "loopover-miner-mcp": "bin/loopover-miner-mcp.js", - "loopover-miner-hosted": "bin/loopover-miner-hosted.js", + "loopover-miner": "dist/bin/loopover-miner.js", + "loopover-miner-mcp": "dist/bin/loopover-miner-mcp.js", + "loopover-miner-hosted": "dist/bin/loopover-miner-hosted.js", }); expect(miner.publishConfig).toEqual(mcp.publishConfig); expect(miner.dependencies["@loopover/engine"]).toBeDefined(); // Upper-bounded (#7613): a floor of >=22.x with an explicit <23.0.0 ceiling excluding the next major. expect(miner.engines.node).toMatch(/^>=22(?:\.\d+){0,2} <23\.0\.0$/); - expect(miner.files).toEqual(expect.arrayContaining(["bin", "lib"])); - // build is split into build:tsc (the real tsc compile; output is committed alongside sources as the - // package's published in-place emit) and + expect(miner.files).toEqual(expect.arrayContaining(["dist"])); + // build is split into build:tsc (the real tsc compile; output lands out-of-place in dist/, gitignored, + // built fresh at publish time -- see tsconfig.json's outDir comment) and // build:verify (a glob-driven node --check pass over every bin/lib .js file, replacing a previously // hand-listed ~119-file chain here that had to be kept in sync by hand). expect(miner.scripts.build).toBe("npm run build:tsc && npm run build:verify"); @@ -54,15 +54,17 @@ describe("loopover-miner package skeleton (#2287)", () => { it("build:verify's syntax check actually covers the CLI bin entry points, not just lib/", () => { // The pre-split build script explicitly node --check'd bin/loopover-miner.js and - // bin/loopover-miner-mcp.js by name; the glob-driven replacement must still reach them. + // bin/loopover-miner-mcp.js by name; the glob-driven replacement must still reach them. Requires a + // prior build (dist/bin, dist/lib populated) -- same precondition as every other test in this file + // that reads real compiled output, and as the package's own real `npm run build`. const result = spawnSync("node", ["scripts/check-syntax.mjs"], { cwd: minerRoot, encoding: "utf8" }); expect(result.status).toBe(0); expect(result.stdout).toContain("node --check passed for all"); - expect(result.stdout).toMatch(/passed for all \d+ files in bin\/ and lib\//); + expect(result.stdout).toMatch(/passed for all \d+ files in dist\/bin\/ and dist\/lib\//); }); it("starts the CLI bin with a node shebang", () => { - const bin = readFileSync(join(minerRoot, "bin/loopover-miner.js"), "utf8"); + const bin = readFileSync(join(minerRoot, "dist/bin/loopover-miner.js"), "utf8"); expect(bin.startsWith("#!/usr/bin/env node\n")).toBe(true); }); diff --git a/test/unit/miner-store-maintenance.test.ts b/test/unit/miner-store-maintenance.test.ts index f2726e2fd9..7982486eef 100644 --- a/test/unit/miner-store-maintenance.test.ts +++ b/test/unit/miner-store-maintenance.test.ts @@ -109,7 +109,7 @@ describe("checkStoreIntegrity (#4834)", () => { // -- the exact gotcha claim-ledger.js's own openClaimLedgerReadOnly already documents and pins. A source-text // check (rather than only a live-connection assertion below) means a future edit that reintroduces the wrong // casing fails immediately, without needing to reason about SQLite's own error-message wording. - const source = readFileSync("packages/loopover-miner/lib/store-maintenance.js", "utf8"); + const source = readFileSync("packages/loopover-miner/dist/lib/store-maintenance.js", "utf8"); expect(source).toContain("new DatabaseSync(dbPath, { readOnly: true })"); expect(source).not.toMatch(/new DatabaseSync\(dbPath,\s*\{\s*readonly:/); }); diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index ff331d0f2b..5dc866b324 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -5,20 +5,19 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect } from "vitest"; -// packages/loopover-mcp ships .ts source only (no committed compiled output -- Vite/esbuild already -// resolves .js-suffixed import specifiers to the sibling .ts by default, which is why in-process imports -// never needed anything special either). Spawning the real CLI as a subprocess still needs a real runnable -// entrypoint, so this runs the .ts directly via Node's own built-in type-stripping (--experimental-strip- -// types, explicit rather than relying on its default-on state so this keeps working regardless of exactly -// which supported Node 22.x patch is running) instead of requiring a prior `npm run build:mcp`. Type -// STRIPPING, not full transformation or type-checking -- that's fine here since `npm run typecheck`/ -// `build:mcp` elsewhere in the gate already own type-correctness, and neither bin/lib file uses syntax -// erasable-only stripping can't handle (enums, namespaces, constructor parameter properties): this harness -// only needs the CLI to actually run. process.execPath (not a bare "node") mirrors scripts/check-syntax.mjs's -// own convention -- guarantees the exact Node binary already running the test, not whatever "node" resolves -// to on PATH. -const NODE_STRIP_TYPES_ARGS = ["--experimental-strip-types"]; -export const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.ts"); +// packages/loopover-mcp compiles out-of-place into dist/ (2026-07-24 migration; see tsconfig.json's outDir +// comment). Spawning bin/loopover-mcp.ts's SOURCE directly via Node's type-stripping used to work with no +// prior build (the entry .ts's internal "./foo.js"-suffixed imports resolved to the in-place-emitted +// sibling .js next to it) -- that stopped working once dist/ physically separated compiled output from +// source: Node's own ESM resolver, unlike Vite/esbuild, never falls back from a missing literal .js +// specifier to a .ts sibling, so a spawned bin/loopover-mcp.ts could no longer resolve its own internal +// lib/*.js imports (nothing lives at lib/*.js anymore, only lib/*.ts). This harness now spawns the real +// compiled dist/bin/loopover-mcp.js as plain JavaScript -- requires `npm run build:mcp` to have run first +// (same precondition ci.yml's "Build MCP" step + this repo's local `npm run test:ci` already satisfy), but +// is otherwise identical: same subprocess, same argv, same env plumbing. process.execPath (not a bare +// "node") mirrors scripts/check-syntax.mjs's own convention -- guarantees the exact Node binary already +// running the test, not whatever "node" resolves to on PATH. +export const bin = join(process.cwd(), "packages/loopover-mcp/dist/bin/loopover-mcp.js"); export const repoOnboardingPackFixture = { repoFullName: "owner/repo", accepted: true, @@ -56,7 +55,7 @@ export async function closeFixtureServer() { export function run(args: string[], env: Record = {}) { try { - return execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { + return execFileSync(process.execPath, [bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -79,7 +78,7 @@ export function runAsync(args: string[], env: Record = {}) { return new Promise((resolve, reject) => { execFile( process.execPath, - [...NODE_STRIP_TYPES_ARGS, bin, ...args], + [bin, ...args], { encoding: "utf8", env: { @@ -104,7 +103,7 @@ export function runAsync(args: string[], env: Record = {}) { * for asserting the shape of the failure output itself (e.g. the --json `{ ok: false, error }` contract). */ export function runExpectingFailure(args: string[], env: Record = {}) { try { - execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { + execFileSync(process.execPath, [bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -118,7 +117,7 @@ export function runExpectingFailure(args: string[], env: Record const failure = error as NodeJS.ErrnoException & { status?: number | null; stdout?: string; stderr?: string }; return { status: failure.status ?? null, stdout: failure.stdout ?? "", stderr: failure.stderr ?? "" }; } - throw new Error(`expected \`node --experimental-strip-types ${bin} ${args.join(" ")}\` to fail`); + throw new Error(`expected \`node ${bin} ${args.join(" ")}\` to fail`); } export function git(cwd: string, ...args: string[]) { diff --git a/test/unit/support/miner-cli-harness.ts b/test/unit/support/miner-cli-harness.ts index 35be9f321c..05de79082f 100644 --- a/test/unit/support/miner-cli-harness.ts +++ b/test/unit/support/miner-cli-harness.ts @@ -19,22 +19,21 @@ export type CliProcessResult = { output: string; }; -// packages/loopover-miner ships .ts source only (no committed compiled output -- Vite/esbuild already -// resolves .js-suffixed import specifiers to the sibling .ts by default, which is why in-process imports -// never needed anything special either). Spawning the real CLI as a subprocess still needs a real runnable -// entrypoint, so this runs the .ts directly via Node's own built-in type-stripping (--experimental-strip- -// types, explicit rather than relying on its default-on state so this keeps working regardless of exactly -// which supported Node 22.x patch is running) instead of requiring a prior `npm run build:miner`. Type -// STRIPPING, not full transformation or type-checking -- that's fine here since `npm run typecheck`/ -// `build:miner` elsewhere in the gate already own type-correctness, and neither bin/lib file uses syntax -// erasable-only stripping can't handle (enums, namespaces, constructor parameter properties): this harness -// only needs the CLI to actually run. process.execPath (not a bare "node") mirrors scripts/check-syntax.mjs's -// own convention -- guarantees the exact Node binary already running the test, not whatever "node" resolves -// to on PATH. -const NODE_STRIP_TYPES_ARGS = ["--experimental-strip-types"]; +// packages/loopover-miner compiles out-of-place into dist/ (2026-07-24 migration; see tsconfig.json's +// outDir comment). Spawning bin/loopover-miner.ts's SOURCE directly via Node's type-stripping used to work +// with no prior build (the entry .ts's internal "./foo.js"-suffixed imports resolved to the in-place- +// emitted sibling .js next to it) -- that stopped working once dist/ physically separated compiled output +// from source: Node's own ESM resolver, unlike Vite/esbuild, never falls back from a missing literal .js +// specifier to a .ts sibling, so a spawned bin/loopover-miner.ts could no longer resolve its own internal +// lib/*.js imports (nothing lives at lib/*.js anymore, only lib/*.ts). This harness now spawns the real +// compiled dist/bin/loopover-miner.js as plain JavaScript -- requires `npm run build:miner` to have run +// first (same precondition ci.yml's "Build miner CLI" step + this repo's local `npm run test:ci` already +// satisfy), but is otherwise identical: same subprocess, same argv, same env plumbing. process.execPath +// (not a bare "node") mirrors scripts/check-syntax.mjs's own convention -- guarantees the exact Node +// binary already running the test, not whatever "node" resolves to on PATH. export const bin = join( process.cwd(), - "packages/loopover-miner/bin/loopover-miner.ts", + "packages/loopover-miner/dist/bin/loopover-miner.js", ); let server: Server | null = null; @@ -45,7 +44,7 @@ export async function closeFixtureServer() { } export function run(args: string[], env: Record = {}) { - return execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { + return execFileSync(process.execPath, [bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -63,7 +62,7 @@ export function runCliResult( args: string[], env: Record = {}, ): CliProcessResult { - const result = spawnSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { + const result = spawnSync(process.execPath, [bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -84,7 +83,7 @@ export function runAsync(args: string[], env: Record = {}) { return new Promise((resolve) => { execFile( process.execPath, - [...NODE_STRIP_TYPES_ARGS, bin, ...args], + [bin, ...args], { encoding: "utf8", env: { diff --git a/test/unit/worker-entry-boundary.test.ts b/test/unit/worker-entry-boundary.test.ts index 43aae669f0..3f0b8bab59 100644 --- a/test/unit/worker-entry-boundary.test.ts +++ b/test/unit/worker-entry-boundary.test.ts @@ -7,7 +7,7 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const srcRoot = join(root, "src"); const WORKER_ENTRY = join(srcRoot, "index.ts"); -const MCP_BIN = join(root, "packages/loopover-mcp/bin/loopover-mcp.js"); +const MCP_BIN = join(root, "packages/loopover-mcp/dist/bin/loopover-mcp.js"); const FORBIDDEN_PATH = /(?:^|\/)visual-agent\//; const FORBIDDEN_IDENTIFIERS = /\b(?:pixelmatch|pngjs|visual-diff|gifenc|sharp)\b/; diff --git a/turbo.json b/turbo.json index 0587bd154e..1a6d60fb7d 100644 --- a/turbo.json +++ b/turbo.json @@ -169,16 +169,39 @@ // and whichever shard's Save lands first can make a LATER shard's Restore see a "hit" for content // it never actually built on that runner, leaving bin/lib empty while the log claims success // (#7705 CI, shard 2 vs. shard 1/3 on the same commit). Declaring the real outputs makes a hit - // restore the actual files instead of only the log. - "outputs": ["bin/**/*.js", "lib/**/*.js"] + // restore the actual files instead of only the log. dist/ prefix since the 2026-07-24 out-of-place + // emit migration (was bin/lib in-place; see tsconfig.json's outDir comment). .tsbuildinfo included + // so a cache HIT restores it alongside dist/, keeping tsc's own incremental state consistent with + // what's on disk -- reproduced locally (not a real CI incident: actions/checkout's default clean: + // true already wipes any stale .tsbuildinfo before every job) that a tsc invocation seeing a stale + // .tsbuildinfo with an empty/missing dist/ silently no-ops, believing everything is already built. + "outputs": ["dist/bin/**/*.js", "dist/lib/**/*.js", ".tsbuildinfo"] }, "@loopover/miner#build:tsc": { "dependsOn": ["^build"], - "cache": false + // Was `cache: false` -- historically to avoid a cache restore stomping this package's hand-written + // .js files while the #7290 migration was still in-flight (see ci.yml's "Build miner CLI" comment). + // #7317 closed that migration out (every bin/lib file is compiler-owned), and the 2026-07-24 dist/ + // migration removed the original in-place-emit hazard entirely -- dist/ is a clean build directory + // turbo fully owns, with no hand-written file a bad restore could ever clobber. Enabling caching now + // (the outputs below mirror @loopover/mcp#build's own #7705-incident-driven declaration: dist/'s + // .js -- plus miner-specific .d.ts, since unlike mcp this package has declaration: true -- and the + // dist/package.json tsc itself emits for that same setting, see check-miner-package.ts's allowlist + // comment) so a cache HIT restores every real file, not just the log. + // .tsbuildinfo included so a cache HIT restores it alongside dist/, keeping tsc's own incremental + // state consistent with what's on disk (same reasoning as @loopover/mcp#build's own outputs above). + "outputs": [ + "dist/bin/**/*.js", + "dist/bin/**/*.d.ts", + "dist/lib/**/*.js", + "dist/lib/**/*.d.ts", + "dist/package.json", + ".tsbuildinfo" + ] }, "@loopover/miner#build:verify": { "dependsOn": ["@loopover/miner#build:tsc"], - "inputs": ["bin/**/*.js", "lib/**/*.js"], + "inputs": ["dist/bin/**/*.js", "dist/lib/**/*.js"], "outputs": [] }, "@loopover/miner#build": { diff --git a/vitest.config.ts b/vitest.config.ts index ca03371938..94b6124740 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,38 +1,8 @@ -import { existsSync } from "node:fs"; -import { dirname, isAbsolute, resolve as resolvePath } from "node:path"; import { defineConfig } from "vitest/config"; const junitPath = process.env.VITEST_JUNIT_PATH; -// In-process imports must resolve the miner/mcp packages' NodeNext ".js"-suffixed specifiers to the -// .ts SOURCE even when the in-place compiled .js exists on disk (both packages emit next to their -// sources -- tsconfig outDir "."; CI builds them before the coverage run, and local checkouts -// accumulate the same gitignored emit). Without this, Vite resolves the literal .js file, v8 -// coverage attributes every hit to the built-file identity (flatlining the .ts to 0% -- the -// 2026-07-24 codecov/patch (0%/99%) incident), and --changed's import-graph tracing can't relate a -// changed .ts to the tests importing it. Deleting the emit before the coverage run instead (the -// first attempt at fixing that incident) broke the OTHER class of tests: subprocess-spawning ones -// (the CLI harnesses and the MCP stdio tests) run under plain Node outside Vite's resolver and -// genuinely need the built .js at spawn time. This plugin serves both: in-process resolution always -// lands on the .ts (correct identity for coverage + tracing), while spawned subprocesses keep the -// built artifacts on disk. Scoped to exactly the two in-place-emit packages -- engine emits to -// dist/ and must keep resolving through its package boundary unchanged. -const IN_PLACE_EMIT_RE = /packages\/loopover-(?:miner|mcp)\/(?:lib|bin)\/.*\.js$/; -const preferTsSourceForInPlaceEmit = { - name: "loopover:prefer-ts-source-for-in-place-emit", - enforce: "pre" as const, - resolveId(source: string, importer: string | undefined) { - if (!importer || !source.endsWith(".js")) return null; - if (!source.startsWith("./") && !source.startsWith("../") && !isAbsolute(source)) return null; - const candidate = isAbsolute(source) ? source : resolvePath(dirname(importer), source); - if (!IN_PLACE_EMIT_RE.test(candidate)) return null; - const tsSibling = `${candidate.slice(0, -3)}.ts`; - return existsSync(tsSibling) ? tsSibling : null; - }, -}; - export default defineConfig({ - plugins: [preferTsSourceForInPlaceEmit], ssr: { noExternal: ["agents", "partyserver"], }, @@ -66,45 +36,39 @@ export default defineConfig({ include: [ "src/**/*.ts", "packages/loopover-engine/src/**/*.ts", - // packages/loopover-{miner,mcp} ship .ts source only (no committed compiled output). Their own - // internal cross-imports and every root test importing into them write NodeNext-style .js-suffixed - // specifiers (required so tsc's real build -- still the actual published npm artifact -- resolves - // correctly) -- Vite/esbuild already resolves those to the sibling .ts when no literal .js exists - // on disk, the same default behavior packages/loopover-engine/src/**'s own .js-suffixed imports - // have always relied on. BUT @vitest/coverage-v8 still tracks each executed module under the id - // Vite resolved it FROM (the requested .js specifier), not the .ts file actually read off disk -- - // confirmed by removing the .js glob entry once and watching every one of these files (genuinely - // executed, genuinely tested) report a flat 0% because coverage.include no longer matched their - // reported id. So both extensions stay listed for the same file: the .js entry is what makes the - // v8 coverage provider find the module AT ALL, and the .ts entry is what makes it display against - // real TypeScript source instead of a phantom .js path that was never written to disk. - "packages/loopover-miner/lib/**/*.js", + // packages/loopover-{miner,mcp} compile out-of-place into dist/ (lib/foo.ts -> dist/lib/foo.js, + // matching packages/loopover-engine's src/ -> dist/ split since the 2026-07-24 migration -- see + // each package's tsconfig.json outDir comment). Only .ts is listed: dist/ is gitignored, never + // scanned by coverage, and holds no .ts of its own, so there's no id-mismatch to work around -- + // Vite/esbuild resolve these packages' NodeNext .js-suffixed import specifiers straight to the + // sibling .ts by ordinary default fallback (nothing lives at the .js path inside lib/bin/ to + // resolve to instead), the same behavior packages/loopover-engine/src/**'s own .js-suffixed + // imports have always relied on. (Before the migration, an in-place compiled .js sitting next to + // its .ts source caused Vite to resolve the literal .js instead, misattributing coverage to the + // built-file identity -- the 2026-07-24 codecov/patch (0%/99%) incident, temporarily worked around + // with a custom resolver plugin before this directory split made the workaround unnecessary.) "packages/loopover-miner/lib/**/*.ts", // bin/loopover-miner-mcp.ts exports createMinerMcpServer, imported in-process by // test/unit/miner-mcp-*.test.ts -- genuinely unit-coverable, same as lib/ above. Its sibling // bin/loopover-miner.ts (the plain CLI dispatcher: no exports, subprocess-only tested via - // test/unit/support/miner-cli-harness.ts, now spawned via Node's own --experimental-strip-types - // rather than a prior `tsc` build) is NOT ignore-listed in codecov.yml -- test/unit/ - // codecov-policy.test.ts (#4864) forbids a blanket exemption for packages/loopover-miner, so it - // stays included and genuinely graded (near-0% today) until it either gains real in-process tests - // or is refactored into a testable export the way bin/loopover-miner-mcp.ts already was. - "packages/loopover-miner/bin/**/*.js", + // test/unit/support/miner-cli-harness.ts, spawned via Node's own --experimental-strip-types) + // is NOT ignore-listed in codecov.yml -- test/unit/codecov-policy.test.ts (#4864) forbids a + // blanket exemption for packages/loopover-miner, so it stays included and genuinely graded + // (near-0% today) until it either gains real in-process tests or is refactored into a testable + // export the way bin/loopover-miner-mcp.ts already was. "packages/loopover-miner/bin/**/*.ts", "packages/discovery-index/src/**/*.ts", // All 5 packages/loopover-mcp/lib/*.ts files (format-table/local-branch/redact-local-path/ // telemetry/cli-error) are imported in-process by test/unit/*.test.ts (cli-error's own // test/unit/mcp-cli-error.test.ts landed in #7409), so a PR touching any of them is covered by // codecov/patch -- that's intended enforcement, not a bug. - "packages/loopover-mcp/lib/**/*.js", "packages/loopover-mcp/lib/**/*.ts", // packages/loopover-mcp/bin/loopover-mcp.ts (~6,600 of ~7,400 lines in the package) is tested - // exclusively via subprocess spawn (test/unit/mcp-cli-*.test.ts, mcp-discovery.test.ts et al, - // through test/unit/support/mcp-cli-harness.ts's execFileSync/StdioClientTransport, now spawned - // via Node's own --experimental-strip-types). Same shape as packages/loopover-miner/bin/ - // loopover-miner.ts above -- and, consistent with that file not getting a codecov.yml exemption - // either (#4864), this isn't ignore-listed: it stays included and genuinely graded until it - // either gains real in-process tests or is refactored into a testable export. - "packages/loopover-mcp/bin/**/*.js", + // exclusively via subprocess spawn (test/unit/mcp-cli-*.test.ts, mcp-discovery.test.ts et al) -- + // same shape as packages/loopover-miner/bin/loopover-miner.ts above -- and, consistent with that + // file not getting a codecov.yml exemption either (#4864), this isn't ignore-listed: it stays + // included and genuinely graded until it either gains real in-process tests or is refactored + // into a testable export. "packages/loopover-mcp/bin/**/*.ts", // review-enrichment is a standalone (non-workspace) package with its own node:test suite; its // coverage is collected separately via `npm run rees:coverage` (c8 over the built dist, remapped @@ -126,11 +90,10 @@ export default defineConfig({ // statements, matches src/env.d.ts's own exclusion above). worker-configuration.d.ts is // wrangler-generated. // - // packages/loopover-miner/lib/**/*.ts (above) also glob-matches its own emitted *.d.ts siblings - // (a ".d.ts" path ends in ".ts" too) -- those aren't real modules and can't be - // parsed as coverage source, so they're excluded the same way src/env.d.ts already is. Same story - // for the *.ts entries under packages/loopover-miner/bin/** and packages/loopover-mcp/{lib,bin}/** - // added above -- each glob-matches its own *.d.ts siblings too. + // packages/loopover-{miner,mcp}'s *.ts include globs above need no *.d.ts exclusion: since the + // 2026-07-24 dist/ migration, tsc emits declarations into dist/lib/ and dist/bin/ (gitignored, + // never scanned by coverage), so lib/** and bin/** now contain only hand-written .ts source -- no + // emitted .d.ts sibling for a ".ts"-ending glob to accidentally sweep in. exclude: [ "src/env.d.ts", "apps/**", @@ -138,10 +101,6 @@ export default defineConfig({ "packages/discovery-index/src/worker.ts", "packages/discovery-index/src/env.d.ts", "packages/discovery-index/worker-configuration.d.ts", - "packages/loopover-miner/lib/**/*.d.ts", - "packages/loopover-miner/bin/**/*.d.ts", - "packages/loopover-mcp/lib/**/*.d.ts", - "packages/loopover-mcp/bin/**/*.d.ts", ], // Emit lcov for Codecov to compute patch (changed-lines) coverage. reporter: ["text", "lcov"],