From 8b0d7536564ccaa8a20ff3294e00bbb06dd5c032 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:45:51 -0700 Subject: [PATCH 1/4] fix(build): migrate loopover-miner/loopover-mcp to out-of-place dist/ emit Completes the proper fix for the 2026-07-24 codecov/patch (0%/99%) incident. #8564 deleted in-place build output before the coverage run; #8568 replaced that with a vitest resolver plugin forcing in-process resolution to .ts regardless of a coexisting .js. Both were workarounds around the real root cause: packages/loopover-{miner,mcp} compiled in-place (tsconfig outDir "."), so compiled .js sat in the same directory as its .ts source -- the actual, recurring bug class (three prior incidents: stale .js shadowing fresh .ts, a no-op incremental build lying about freshness, then the coverage-attribution bug). This migrates both packages to out-of-place emit (outDir "dist"), matching how packages/loopover-engine already builds. .ts source and compiled .js output now live in physically separate directories, so there is never a same-directory collision for any resolver to work around -- the #8568 plugin is removed as dead code. Turbo caching fixed alongside the directory move (a real, deferred optimization surfaced by grep'ing this exact history): build:tsc's `cache: false` was explicit migration-era caution a prior PR (#7317) already flagged as safe to remove once bin/lib became fully compiler-owned; the dist/ split removes the original hazard entirely. Both packages' build tasks now declare real outputs (including .tsbuildinfo, so a cache HIT keeps tsc's own incremental state consistent with what's restored) instead of never caching at all. Every literal reference to the old in-place bin/lib layout is updated: the shared CLI test harnesses (which had to give up spawning bin/*.ts directly via --experimental-strip-types -- that only worked because Node's resolver found the entry's internal lib/*.js imports sitting right next to it; it does not fall back .js->.ts the way Vite/esbuild do), ~40 test files with hardcoded subprocess/readFileSync paths, 4 fixture scripts, the pack-check scripts' allowlists, check-syntax.mjs in both packages, the DEPLOYMENT.md audit script + its documented paths, and package.json's bin/files fields. Two production-source bugs found and fixed properly, not patched around: bin/loopover-mcp.ts and lib/status.ts both compute paths relative to their own file location (own package.json, CHANGELOG.md, the monorepo-sibling loopover-engine package) -- these files are ALSO imported in-process by tests that resolve against the real .ts source, so a single hardcoded relative depth cannot be correct for both contexts. Both now try the source-relative depth first, falling back to the dist-relative depth, so they work correctly however they're currently loaded. bin/loopover-mcp.ts's dynamic `import("@loopover/miner/lib/claim-ledger.js")` -- a published-package subpath import into miner's internals, invisible to any relative-path grep -- is fixed via an explicit `exports` map in miner's package.json instead of hardcoding "dist/" into mcp's own source, so the public contract between the two packages stays stable independent of miner's internal layout. package-lock.json's bin fields resynced (npm mirrors workspace packages' bin targets there); node_modules/.bin symlinks regenerated via a real `npm install` (--package-lock-only does not relink them). Docs: contributing-to-loopover's reference.md and SKILL.md corrected -- they previously said miner/mcp tests "run straight off the .ts" with no build needed, which is no longer true for the CLI-harness and stdio-transport-spawning tests now that dist/ is a separate directory. Validated: full unsharded suite (21,541/21,541 passing), typecheck clean, both real compiled CLIs execute and self-report their version correctly, turbo cache save+restore verified to actually restore dist/bin, dist/lib, and dist/package.json on a hit, and the monorepo package-check scripts pass against the real built workspace. --- .../skills/contributing-to-loopover/SKILL.md | 7 ++ .../contributing-to-loopover/reference.md | 14 ++- .github/workflows/ci.yml | 10 +- .gitignore | 21 ++--- Dockerfile | 2 +- package-lock.json | 8 +- packages/loopover-mcp/bin/loopover-mcp.ts | 21 ++++- packages/loopover-mcp/package.json | 7 +- .../loopover-mcp/scripts/check-syntax.mjs | 4 +- .../scripts/strip-bin-sourcemap.mjs | 8 +- packages/loopover-mcp/tsconfig.json | 20 ++-- packages/loopover-miner/DEPLOYMENT.md | 40 ++++---- packages/loopover-miner/lib/status.ts | 19 +++- packages/loopover-miner/package.json | 19 ++-- packages/loopover-miner/scripts/benchmark.mjs | 4 +- .../loopover-miner/scripts/check-syntax.mjs | 4 +- .../scripts/cross-repo-evaluation.mjs | 2 +- .../scripts/egress-firewall-entrypoint.sh | 2 +- packages/loopover-miner/tsconfig.json | 21 +++-- scripts/check-miner-deployment-docs.ts | 8 +- scripts/check-miner-package.ts | 23 +++-- scripts/mcp-package-allowlist.ts | 12 +-- .../claim-and-hold-child.mjs | 2 +- .../miner-concurrent-stores/claim-child.mjs | 2 +- .../miner-concurrent-stores/dequeue-child.mjs | 2 +- .../acquire-child.mjs | 2 +- test/unit/check-mcp-package.test.ts | 16 ++-- test/unit/check-miner-package.test.ts | 56 ++++++------ test/unit/forbidden-content.test.ts | 4 +- test/unit/mcp-cli-boundary-tests-tool.test.ts | 2 +- test/unit/mcp-cli-bounty-advisory.test.ts | 2 +- test/unit/mcp-cli-burden-forecast.test.ts | 2 +- test/unit/mcp-cli-completion-spec.test.ts | 2 +- test/unit/mcp-cli-draft-pr-body.test.ts | 2 +- .../mcp-cli-evaluate-escalation-tool.test.ts | 2 +- .../mcp-cli-explain-gate-disposition.test.ts | 2 +- test/unit/mcp-cli-find-opportunities.test.ts | 2 +- test/unit/mcp-cli-help.test.ts | 2 +- test/unit/mcp-cli-intake-idea-tool.test.ts | 2 +- test/unit/mcp-cli-intake-tools.test.ts | 2 +- test/unit/mcp-cli-issue-rag.test.ts | 2 +- test/unit/mcp-cli-label-audit.test.ts | 2 +- test/unit/mcp-cli-maintain-tools.test.ts | 2 +- test/unit/mcp-cli-maintain.test.ts | 2 +- test/unit/mcp-cli-maintainer-lane.test.ts | 2 +- test/unit/mcp-cli-maintainer-noise.test.ts | 2 +- test/unit/mcp-cli-monitor-open-prs.test.ts | 2 +- .../mcp-cli-open-pr-pressure-tool.test.ts | 2 +- .../mcp-cli-plan-idea-claims-tool.test.ts | 2 +- test/unit/mcp-cli-plan-scorer-tools.test.ts | 2 +- .../mcp-cli-pr-ai-review-findings.test.ts | 2 +- test/unit/mcp-cli-pr-outcomes.test.ts | 2 +- test/unit/mcp-cli-pr-reviewability.test.ts | 2 +- .../mcp-cli-progress-snapshot-tool.test.ts | 2 +- .../mcp-cli-repo-outcome-patterns.test.ts | 2 +- .../unit/mcp-cli-results-payload-tool.test.ts | 2 +- test/unit/mcp-cli-review-risk.test.ts | 2 +- test/unit/mcp-cli-test-evidence-tool.test.ts | 2 +- test/unit/mcp-cli-tools.test.ts | 2 +- test/unit/mcp-cli-upstream-drift.test.ts | 2 +- .../mcp-cli-validate-config-offline.test.ts | 2 +- test/unit/mcp-cli-write-tools.test.ts | 2 +- test/unit/mcp-discovery.test.ts | 2 +- test/unit/mcp-feasibility-gate.test.ts | 2 +- test/unit/mcp-lint-pr-text.test.ts | 2 +- test/unit/mcp-local-check-slop-risk.test.ts | 2 +- .../mcp-local-telemetry-chokepoint.test.ts | 2 +- test/unit/mcp-release-candidate.test.ts | 12 +-- test/unit/miner-attempt-log.test.ts | 2 +- test/unit/miner-claim-ledger.test.ts | 2 +- test/unit/miner-deployment-docs-audit.test.ts | 9 +- test/unit/miner-event-ledger.test.ts | 2 +- test/unit/miner-mcp-tool-docs-parity.test.ts | 2 +- test/unit/miner-package-skeleton.test.ts | 20 ++-- test/unit/miner-store-maintenance.test.ts | 2 +- test/unit/support/mcp-cli-harness.ts | 35 ++++--- test/unit/support/miner-cli-harness.ts | 33 ++++--- test/unit/worker-entry-boundary.test.ts | 2 +- turbo.json | 31 ++++++- vitest.config.ts | 91 +++++-------------- 80 files changed, 356 insertions(+), 323 deletions(-) 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..3f102ee2b5 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 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..8924335991 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -5,7 +5,7 @@ import { createHash } from "node:crypto"; import { closeSync, constants as fsConstants, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { delimiter, dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { buildFeasibilityVerdict, buildPrTextLint, buildGateDispositions, buildPublicPrBodyDraft } from "@loopover/engine"; @@ -48,10 +48,25 @@ 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"; +// Path to a file next to this package's own root, computed relative to THIS module's own on-disk +// location -- which differs by one directory level depending on how this file is currently running: +// imported in-process (e.g. by the vitest unit tests that exercise the CLI dispatcher + stdio tools +// directly against bin/loopover-mcp.ts, per the invokedPath check below) resolves relative to the +// real source bin/, while a real CLI invocation resolves relative to the compiled dist/bin/ (one +// level deeper, since the 2026-07-24 dist/ migration; 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. +function resolveOwnPackageSiblingPath(...segments: string[]): URL { + const here = dirname(fileURLToPath(import.meta.url)); + const fromBin = join(here, "..", ...segments); + if (existsSync(fromBin)) return pathToFileURL(fromBin); + return pathToFileURL(join(here, "..", "..", ...segments)); +} + // 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(resolveOwnPackageSiblingPath("package.json"), "utf8")); const defaultApiUrl = "https://api.loopover.ai"; const legacyDefaultApiUrls = new Set([ @@ -71,7 +86,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 = resolveOwnPackageSiblingPath("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 diff --git a/packages/loopover-mcp/package.json b/packages/loopover-mcp/package.json index 930adad187..6217172afa 100644 --- a/packages/loopover-mcp/package.json +++ b/packages/loopover-mcp/package.json @@ -26,15 +26,12 @@ "access": "public" }, "bin": { - "loopover-mcp": "bin/loopover-mcp.js" + "loopover-mcp": "dist/bin/loopover-mcp.js" }, "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/lib/status.ts b/packages/loopover-miner/lib/status.ts index d2e9f0b126..f0e037cd28 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). @@ -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"), ); } 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..b6a4bbf25b 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/dist/lib/deployment-docs-audit.d.ts"; 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"], From d52c078bf2bac10eb57dfbed329c45e0fcfad5d4 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:56:59 -0700 Subject: [PATCH 2/4] refactor(build): replace relative-depth self-reference guessing with Node's own package self-referencing The previous commit's fix for the cross-context (in-process test vs compiled dist/) self-relative-path problem was a "try depth N, then depth N+1" heuristic (resolveOwnPackageSiblingPath / resolveMonorepoSiblingPath). It worked, but it's not the correct tool for the job: Node has a purpose-built, standard mechanism for exactly this -- a package importing its own files by its own published name ("self-referencing"), resolved via the package.json "exports" map the same way any external "@loopover/x/..." import would be. That's robust by construction (walks up through node_modules from the calling file's own location, landing on the one real file regardless of that file's current directory depth) rather than by guessing candidate depths. Adds a minimal "exports" map to both packages' package.json (own package.json + the one or two other self-referenced files each needs) and switches every "own package.json"-style self-reference -- bin/loopover-mcp.ts (package.json, CHANGELOG.md), lib/version.ts's static JSON import, lib/status.ts's two require() calls, and bin/loopover-miner-mcp.ts -- to import.meta.resolve()/self-referencing require() against the published package name instead. The one remaining depth-guessing fallback (lib/status.ts's monorepo-sibling loopover-engine lookup) is intentionally left as-is: it's a last-resort fallback for when real node_modules resolution has already failed, so a self-referencing lookup (which uses the identical resolution mechanism) wouldn't diversify the fallback at all -- trying both plausible sibling-directory depths is the actually-correct strategy for that specific case, not a shortcut. Validated: typecheck clean, both packages rebuild cleanly, full unsharded suite still 21,541/21,541 passing, and both self-referencing subpaths resolve correctly via direct node -e checks. --- packages/loopover-mcp/bin/loopover-mcp.ts | 31 +++++++++---------- packages/loopover-mcp/package.json | 4 +++ .../loopover-miner/bin/loopover-miner-mcp.ts | 18 ++++++----- packages/loopover-miner/lib/status.ts | 4 +-- packages/loopover-miner/lib/version.ts | 8 ++++- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 8924335991..40a4e4c7cb 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -5,7 +5,7 @@ import { createHash } from "node:crypto"; import { closeSync, constants as fsConstants, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { delimiter, dirname, join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { buildFeasibilityVerdict, buildPrTextLint, buildGateDispositions, buildPublicPrBodyDraft } from "@loopover/engine"; @@ -48,25 +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"; -// Path to a file next to this package's own root, computed relative to THIS module's own on-disk -// location -- which differs by one directory level depending on how this file is currently running: -// imported in-process (e.g. by the vitest unit tests that exercise the CLI dispatcher + stdio tools -// directly against bin/loopover-mcp.ts, per the invokedPath check below) resolves relative to the -// real source bin/, while a real CLI invocation resolves relative to the compiled dist/bin/ (one -// level deeper, since the 2026-07-24 dist/ migration; 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. -function resolveOwnPackageSiblingPath(...segments: string[]): URL { - const here = dirname(fileURLToPath(import.meta.url)); - const fromBin = join(here, "..", ...segments); - if (existsSync(fromBin)) return pathToFileURL(fromBin); - return pathToFileURL(join(here, "..", "..", ...segments)); +// 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(resolveOwnPackageSiblingPath("package.json"), "utf8")); +const ownPackageJson = JSON.parse(readFileSync(resolveOwnPackageFile("@loopover/mcp/package.json"), "utf8")); const defaultApiUrl = "https://api.loopover.ai"; const legacyDefaultApiUrls = new Set([ @@ -86,7 +85,7 @@ const decisionPackCacheSchemaVersion = 1; const decisionPackCacheMaxEntries = 25; const decisionPackCacheMaxBytes = 512 * 1024; const cliTextFileMaxBytes = 1024 * 1024; -const changelogPath = resolveOwnPackageSiblingPath("CHANGELOG.md"); +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 diff --git a/packages/loopover-mcp/package.json b/packages/loopover-mcp/package.json index 6217172afa..6bdaf21c7e 100644 --- a/packages/loopover-mcp/package.json +++ b/packages/loopover-mcp/package.json @@ -28,6 +28,10 @@ "bin": { "loopover-mcp": "dist/bin/loopover-mcp.js" }, + "exports": { + "./package.json": "./package.json", + "./CHANGELOG.md": "./CHANGELOG.md" + }, "files": [ "dist", "scripts", 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 f0e037cd28..8c299dcb1c 100644 --- a/packages/loopover-miner/lib/status.ts +++ b/packages/loopover-miner/lib/status.ts @@ -132,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; @@ -275,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; From f808d662f1549dd535b95975c7e1699f2207a832 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:18:52 -0700 Subject: [PATCH 3/4] fix(build): remove two remaining build-order dependencies CI caught MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both bugs share one root cause: I locally validated everything against a worktree where dist/ was already built from earlier test runs, so I never actually exercised a truly clean checkout. CI did, on both validate-tests and validate-code, and caught what local testing missed. 1. bin/loopover-mcp.ts's dynamic `import("@loopover/miner/lib/ claim-ledger.js")` — now resolvable via the "exports" map added in the prior commit — made tsc attempt to statically resolve and type that module during MCP's own build. CI's validate-tests job builds MCP before miner (unchanged, pre-existing order), so miner's dist/ doesn't exist yet at that point: TS2307. This import is explicitly optional and already try/catch-guarded at runtime for "loopover- miner genuinely unresolvable" -- it should never have had a build-time dependency on miner in the first place. Fixed by building the specifier from concatenated string parts instead of a literal, so tsc doesn't attempt static resolution at all, paired with a hand-shaped local type (no coupling to miner's actual types needed for three destructured fields), matching the code's own already-optional runtime shape. 2. test/unit/miner-deployment-docs-audit.test.ts's `import type { DeploymentDocsReality } from ".../dist/lib/deployment-docs- audit.d.ts"` (added in the prior commit) made the root `npm run typecheck` -- which validate-code runs without building miner first -- fail the same way. Unlike the runtime readFileSync calls in that same file (which genuinely need real compiled output on disk and correctly still point at dist/), a TYPE-ONLY import has no such requirement: NodeNext module resolution already resolves a plain .js-suffixed specifier to its sibling .ts source for type information, the exact mechanism the adjacent value import on the line above already relies on. Pointing the type import at the same lib/deployment-docs-audit.js specifier (not dist/) gets real types with zero build dependency, verified by re-running `npm run typecheck` against a directory tree with no dist/ built for either package at all. Verified this time: full clean state (rm -rf both packages' dist/ and .tsbuildinfo) --> typecheck passes with nothing built --> full rebuild --> unsharded suite 21,568/21,568 passing --> both pack-check scripts pass. Rebased onto origin/main first (11 commits had landed, including overlapping changes to package.json/package-lock.json). --- packages/loopover-mcp/bin/loopover-mcp.ts | 23 +++++++++++++++++-- test/unit/miner-deployment-docs-audit.test.ts | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 40a4e4c7cb..0fb025d8b1 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -578,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/test/unit/miner-deployment-docs-audit.test.ts b/test/unit/miner-deployment-docs-audit.test.ts index b6a4bbf25b..7a6405fbc4 100644 --- a/test/unit/miner-deployment-docs-audit.test.ts +++ b/test/unit/miner-deployment-docs-audit.test.ts @@ -12,7 +12,7 @@ import { scanEnvVarTokens, scanRegisteredCommands, } from "../../packages/loopover-miner/lib/deployment-docs-audit.js"; -import type { DeploymentDocsReality } from "../../packages/loopover-miner/dist/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"); From 18e48b79b80a2263e325a5a289e9230e1aba800c Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:33:48 -0700 Subject: [PATCH 4/4] fix(ci): update the last stale bin/ path references in ci.yml Missed this during the migration despite reading this exact file multiple times earlier: the "Verify MCP/miner CLI binaries were actually built" tripwire step still checked the old in-place packages/loopover-{mcp,miner}/bin/loopover-*.js paths. Since those binaries now build into dist/bin/ (see tsconfig.json's outDir comment), the tripwire was correctly firing -- the files genuinely aren't at the old path anymore -- failing validate-tests right after the two build steps it's meant to guard, exactly as designed, just against the wrong path. Also corrected the explanatory comment above that step and the "Build MCP"/"Build miner CLI" steps: it described the pre-migration mechanism (CLI harnesses spawning bin/*.ts directly via --experimental-strip- types, relying on the compiled .js sitting in-place next to it) which this migration already replaced (the harnesses now spawn the real compiled dist/bin/*.js directly). Left everything else the earlier grep flagged alone -- the other packages/loopover-miner/lib/*.js mentions elsewhere in ci.yml are comments about *.js-suffixed import SPECIFIERS (which still correctly resolve to their sibling .ts either way, unaffected by where compiled output lands) or pre-existing staleness unrelated to this migration (release-selfhost.yml's "(committed, pre-built)" predates even the #7290 gitignore-the- compiled-output migration). Verified this time by exactly replicating the failing job's own sequence locally: build engine, force-build MCP, force-build miner, then the verify step's own loop -- both paths now resolve. Also reconfirmed clean-state typecheck and the full unsharded suite (21,568/21,568) after this change. --- .github/workflows/ci.yml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f102ee2b5..844e56d9d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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