diff --git a/.claude/skills/contributing-to-loopover/SKILL.md b/.claude/skills/contributing-to-loopover/SKILL.md index 7a81638fd6..d05ca48883 100644 --- a/.claude/skills/contributing-to-loopover/SKILL.md +++ b/.claude/skills/contributing-to-loopover/SKILL.md @@ -202,7 +202,7 @@ Run the matching command(s) and **commit the regenerated file(s)** — CI fails | A Cloudflare binding/var in `wrangler.jsonc` | `npm run cf-typegen` | `worker-configuration.d.ts` | | Drizzle schema (`src/db/schema.ts`) | `npm run drizzle:generate` | the new `migrations/NNNN_*.sql` | | Added a raw-SQL migration | (none — just author it) | next **contiguous** `migrations/NNNN_snake.sql` | -| `src/selfhost/**` (or a few other scanned files — see `scripts/gen-selfhost-env-reference.mjs`'s `DEFAULT_SOURCE_ROOTS`) adding/removing an `env.SOMETHING` read | `npm run selfhost:env-reference` | `apps/loopover-ui/src/lib/selfhost-env-reference.ts` — the doc cites the file only (not `file:line`, deliberately, so an unrelated line shift elsewhere in the file never makes this go stale) | +| `src/selfhost/**` (or a few other scanned files — see `scripts/gen-selfhost-env-reference.ts`'s `DEFAULT_SOURCE_ROOTS`) adding/removing an `env.SOMETHING` read | `npm run selfhost:env-reference` | `apps/loopover-ui/src/lib/selfhost-env-reference.ts` — the doc cites the file only (not `file:line`, deliberately, so an unrelated line shift elsewhere in the file never makes this go stale) | | CLI command surface | `npm run command-reference` | the generated command-reference doc | | UI files (`apps/loopover-ui/**`) | `npm --workspace @loopover/ui run format` | formatted files | @@ -221,9 +221,9 @@ npm audit --audit-level=moderate # the dependency-review job's local eq `npm run test:ci` runs, and must pass, **all of**: `actionlint`, `db:migrations:check`, `db:schema-drift:check`, `selfhost:env-reference:check`, `selfhost:validate-observability`, -`cf-typegen:check`, `build:mcp:check`, `build:miner:check`, `typecheck`, `test:coverage`, `test:engine-parity`, `test:live-gate-parity`, `test:driver-parity`, the -`@loopover/engine` workspace's own test run, `test:workers`, `test:mcp-pack`, -`test:miner-pack`, `rees:test`, `ui:openapi:check`, `ui:openapi:settings-parity`, +`cf-typegen:check`, `typecheck`, `test:coverage`, `test:engine-parity`, `test:live-gate-parity`, `test:driver-parity`, the +`@loopover/engine` workspace's own test run, `test:workers`, `build:mcp`, `test:mcp-pack`, +`build:miner`, `test:miner-pack`, `rees:test`, `ui:openapi:check`, `ui:openapi:settings-parity`, `ui:version-audit`, `docs:drift-check`, `manifest:drift-check`, `engine-parity:drift-check`, `command-reference:check`, `ui:lint`, `ui:typecheck`, `ui:test`, `ui:build`. If any step fails, fix it and re-run — do not push a red tree. (Full per-check table in `reference.md`; check `package.json`'s diff --git a/.claude/skills/contributing-to-loopover/reference.md b/.claude/skills/contributing-to-loopover/reference.md index 9442e1300b..14cfdb0124 100644 --- a/.claude/skills/contributing-to-loopover/reference.md +++ b/.claude/skills/contributing-to-loopover/reference.md @@ -40,6 +40,13 @@ jobs run only if their path filter matched; on push to `main`, everything runs. | mcp → pack | tarball hygiene | `npm run test:mcp-pack` | unexpected/forbidden file or stale README in the npm tarball | | miner → build | miner engine/pkg build | `npm run build:miner` | `@loopover/{engine,miner}` build error | | 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. + | 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`) | | ui → openapi settings-parity | schema/type structural diff | `npm run ui:openapi:settings-parity` | `RepositorySettingsSchema` (src/openapi/schemas.ts) is missing a field the `RepositorySettings` type has | @@ -60,7 +67,7 @@ jobs run only if their path filter matched; on push to `main`, everything runs. Every `vitest.config.ts` in the repo (root, `vitest.workers.config.ts`, and every workspace with its own config) wires this as `globalSetup`, so it runs once before any test file regardless of how vitest was invoked — including a direct `npx vitest run test/unit/.test.ts`, which this doc's own §6 ("Iterate, -then verify") recommends for fast iteration. `scripts/check-node-version.mjs`'s `pretest*` hooks +then verify") recommends for fast iteration. `scripts/check-node-version.ts`'s `pretest*` hooks (`package.json`) only cover 5 high-traffic npm script names (`test`, `test:ci`, `test:coverage`, `test:workers`, `ui:test`) as a genuinely-faster fail there (before npm even spawns vitest) — they are a nicety on top of the globalSetup guarantee, not a substitute for it. An earlier version of this guard was diff --git a/.github/workflows/ci-duration-report.yml b/.github/workflows/ci-duration-report.yml index f3cecffd40..2c379b3335 100644 --- a/.github/workflows/ci-duration-report.yml +++ b/.github/workflows/ci-duration-report.yml @@ -33,7 +33,7 @@ jobs: - name: Pull ci.yml run stats env: GITHUB_TOKEN: ${{ github.token }} - run: node scripts/ci-duration-report.mjs --days=7 --output=ci-duration-report.json + run: node --experimental-strip-types scripts/ci-duration-report.ts --days=7 --output=ci-duration-report.json - name: Write summary run: | node <<'NODE' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6b7b39d5d..fdc225b786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,8 +143,8 @@ jobs: - 'apps/loopover-extension/**' - 'apps/loopover-miner-extension/**' - 'packages/loopover-ui-kit/**' - - 'scripts/build-extension.mjs' - - 'scripts/build-miner-extension.mjs' + - 'scripts/build-extension.ts' + - 'scripts/build-miner-extension.ts' - 'package.json' - 'package-lock.json' # Backend changes that can drift the OpenAPI contract the UI type-checks against, but say @@ -179,7 +179,8 @@ jobs: # 6 of the 7 MCP CLI-cluster test files, and ONLY these 6, are truly self-contained w.r.t. root # src/**: verified by direct-import inspection that test/unit/mcp-cli-*.test.ts (5 files) and # their shared test/unit/support/mcp-cli-harness.ts import nothing but node:* builtins + vitest, - # and mcp-discovery.test.ts spawns packages/loopover-mcp/bin/loopover-mcp.js as a real + # and mcp-discovery.test.ts spawns packages/loopover-mcp/bin/loopover-mcp.ts (gitignored + # compiled output; Node's own type-stripping runs the .ts directly, no build needed) as a real # subprocess via StdioClientTransport -- none of the 6 ever load root src/ in-process. This mirrors # the `mcp` filter's own established trust boundary above (a self-contained package build). They are # NOT self-contained w.r.t. packages/loopover-engine/**, though: loopover-mcp now has a real @@ -218,7 +219,7 @@ jobs: # Both miner-package test files are self-contained w.r.t. root src/**, the same trust boundary as # mcpCliHarness above: check-miner-package.test.ts only spawns scripts/check-miner-package.mjs as a # real subprocess (node:child_process + vitest, nothing else), and miner-calibration-types.test.ts - # only imports the scaffolded packages/loopover-miner/lib/calibration.js -- neither ever loads + # only imports the scaffolded packages/loopover-miner/lib/calibration.ts -- neither ever loads # root src/ in-process. Mirrors mcpCliHarness's filter shape for the same reason (tooling/config # changes that could plausibly affect how these tests run, plus the miner package + the test files # themselves), swapping in the miner-specific package and test files. @@ -398,10 +399,13 @@ jobs: if: ${{ github.event_name == 'push' || needs.changes.outputs.observability == 'true' }} env: NODE_OPTIONS: "" - run: node scripts/validate-observability-configs.mjs + run: node --experimental-strip-types scripts/validate-observability-configs.ts # Runs ahead of Typecheck AND "Test with coverage" (#ci-engine-build-order): src/mcp/find-opportunities.ts # (root backend, since #2281/#3985) imports packages/loopover-miner/lib/opportunity-fanout.js -- - # committed, pre-built JS -- which itself imports @loopover/engine. That package's dist/ is + # a .js-suffixed specifier esbuild/Vite resolve straight to the real (gitignored, uncompiled) + # .ts source, same as @loopover/engine's own src/**, so THIS half needs no build at all -- which + # itself imports @loopover/engine THE PACKAGE, though, via a bare `@loopover/engine` specifier + # elsewhere in the chain, and THAT resolves through node_modules to dist/. That package's dist/ is # gitignored and only exists after this build step, so ANY backend typecheck or test run needs it built # first, not just a PR that touches packages/loopover-engine/** directly (this step's original, too- # narrow trigger). Originally ran after coverage (broke test:coverage on PRs that didn't touch the @@ -510,49 +514,36 @@ jobs: # old npm run build:mcp -- turbo resolves @loopover/engine as its topological dependency (mcp's own # package.json depends on @loopover/engine) and reuses the "Restore Turborepo cache" above, so an # engine-only PR's already-built engine is never rebuilt a second time here. + # packages/loopover-mcp ships .ts source only -- no compiled .js/.d.ts is committed (gitignored; + # see vitest.config.ts's resolveMinerMcpTsSourcePlugin for how tests run against the .ts directly + # with no build step). This "Build MCP" step still exists because the PACKAGE ITSELF has to ship + # runnable .js in its published npm tarball (npm install -g gives you plain Node, no TS toolchain), + # and "MCP package check" right below needs that real build output on disk to validate. - name: Build MCP if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }} run: npx turbo run build --filter=@loopover/mcp - # packages/loopover-mcp/{bin,lib} compile real TypeScript in place (tsc's outDir === rootDir, so - # e.g. lib/foo.ts emits lib/foo.js right next to it) and commit that emitted output -- the package - # ships as an installable CLI (npm install -g), so a runnable .js has to exist in the published - # tarball, and test/**/*.test.ts imports the emitted .js by its literal specifier, not the .ts - # source. "Build MCP" above (this job) and the "Test with coverage" job below run as separate GitHub - # Actions jobs with independent checkouts, so nothing in this job's build step can ever self-heal a - # stale commit before the coverage job's tests run against it -- a contributor who edits .ts and - # forgets to rebuild+commit the .js gets tests silently exercising old behavior with no CI signal, - # same drift-class gap the Selfhost/Miner env-reference checks above were added to close. This - # fails loudly instead: the build one step up already regenerated bin/lib in place, so any git - # status output here means the committed .js/.d.ts didn't match what that build just produced. - - name: MCP build drift check - if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }} - run: node scripts/check-build-drift.mjs mcp - name: MCP package check if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }} run: npm run test:mcp-pack - # 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, 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 ran them as cached prerequisite tasks - # (declaring a task as a dependsOn prerequisite doesn't replace a parent task's own script body). - # Calling the aggregate here would silently double both the tsc compile and the syntax check on - # every run, negating the caching win this comment describes -- confirmed by reproducing the double - # execution locally before switching to this direct form. This also finally activates the - # build:tsc/build:verify split from Phase 0, which no ci.yml step exercised until now. + # 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, + # 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 + # ran them as cached prerequisite tasks (declaring a task as a dependsOn prerequisite doesn't replace + # a parent task's own script body). Calling the aggregate here would silently double both the tsc + # compile and the syntax check on every run, negating the caching win this comment describes -- + # confirmed by reproducing the double execution locally before switching to this direct form. This + # also finally activates the build:tsc/build:verify split from Phase 0, which no ci.yml step + # exercised until now. - name: Build miner CLI if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' || needs.changes.outputs.engine == 'true' }} run: npx turbo run build:tsc build:verify --filter=@loopover/miner - # Same "separate job, no self-heal" drift-class gap as MCP's own build drift check above, and the - # ORIGINAL motivating case for it: packages/loopover-miner/lib/cross-repo-evaluation.{ts,js,d.ts} - # are real files a contributor edits directly, not obscure internals. - - name: Miner build drift check - if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' || needs.changes.outputs.engine == 'true' }} - run: node scripts/check-build-drift.mjs miner # loopover-miner depends on @loopover/engine for real too (same relationship as loopover-mcp above # -- packages/loopover-miner/package.json lists it as a dependency), so this and "Build miner CLI" # above now also trigger on an engine-only PR, matching "Build MCP"/"MCP package check"'s existing @@ -561,7 +552,7 @@ jobs: - name: Miner package check if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' || needs.changes.outputs.engine == 'true' }} run: npm run test:miner-pack - # scripts/check-miner-deployment-docs.mjs reads packages/loopover-engine/src/miner/**/*.ts + # scripts/check-miner-deployment-docs.ts reads packages/loopover-engine/src/miner/**/*.ts # (ENGINE_MINER_DIR) to build the registered-commands list checked against DEPLOYMENT.md, so an # engine-only PR touching that directory used to skip this audit entirely. - name: Miner deployment docs audit @@ -868,7 +859,7 @@ jobs: # miner + ui-kit + ui + extension) was very likely discarded every run in favor of a shard's # engine-only one. Splitting the prefixes gives each job's own lineage instead of racing; within THIS # prefix, the 6 shards below still race each other for one slot, which is fine since they all save the - # exact same content (only @loopover/engine#build runs in this job) -- whichever shard wins, the + # exact same content (engine + mcp + miner builds all run in this job) -- whichever shard wins, the # content is equivalent, unlike the cross-job case this split just fixed. - name: Restore Turborepo cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -879,6 +870,50 @@ jobs: turbo-tests-${{ hashFiles('package-lock.json') }}- - 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. + # + # --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 + # shards still race on ONE shared turbo-tests- cache key/slot (below), and a hit-from-a-sibling-shard + # is exactly the kind of cross-job interaction that's expensive to keep re-verifying under. --force + # makes this job's own correctness independent of that entirely: it always executes for real here, + # never trusts a hit (from any source, this run or a prior one) for the two packages hundreds of + # downstream tests treat as load-bearing. The packages are small (a few seconds each, confirmed + # locally) -- this is a deliberate, small, fixed cost traded for eliminating an entire class of + # cache-hit-but-file-missing failures, not a workaround. Unconditional (no `if:`), matching "Build + # engine package" immediately above; --force doesn't skip the write side, so this run's own build + # still contributes a correct, restorable cache entry for anything else that reads the turbo-tests- + # prefix (this job's own next run, or a future validate-tests-only invocation). + - name: Build MCP + run: npx turbo run build --filter=@loopover/mcp --force + - name: Build miner CLI + run: npx turbo run build:tsc build:verify --filter=@loopover/miner --force + # Fail fast and unambiguously, right here, if either binary is somehow still missing after the two + # --force builds above -- instead of dozens of confusing per-test MODULE_NOT_FOUND/ENOENT failures far + # downstream in "Test with coverage" that all look unrelated to each other and to this step. This + # should be structurally impossible now (both builds above are forced, never cache-skipped), but it's + # a one-line, load-bearing tripwire for a class of bug that has already cost real debugging time twice. + - 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 + 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 + fi + done - name: Save Turborepo cache if: ${{ !cancelled() }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/.github/workflows/gittensor-impact.yml b/.github/workflows/gittensor-impact.yml index fa4b936d70..35984a2494 100644 --- a/.github/workflows/gittensor-impact.yml +++ b/.github/workflows/gittensor-impact.yml @@ -1,6 +1,6 @@ name: Gittensor Impact -# Renders the README contributor-impact card with scripts/gittensor-impact-card.mjs +# Renders the README contributor-impact card with scripts/gittensor-impact-card.ts # (our own script, styled to this repo's brand — see apps/loopover-ui/src/styles.css) # and publishes it to the gittensor-impact-assets branch, which the README embeds # directly. No third-party action: this replaced matthewevans/gittensor-impact-action @@ -39,7 +39,7 @@ jobs: node-version: 22.23.1 - name: Render impact card - run: node scripts/gittensor-impact-card.mjs JSONbored/loopover gittensor-impact-dark.svg + run: node --experimental-strip-types scripts/gittensor-impact-card.ts JSONbored/loopover gittensor-impact-dark.svg - name: Publish to gittensor-impact-assets env: diff --git a/.github/workflows/mcp-release-please.yml b/.github/workflows/mcp-release-please.yml index a64b1349f9..25dfec4afc 100644 --- a/.github/workflows/mcp-release-please.yml +++ b/.github/workflows/mcp-release-please.yml @@ -126,8 +126,8 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - trusted_sync_script="${RUNNER_TEMP}/sync-release-lockfile-versions.mjs" - cp scripts/sync-release-lockfile-versions.mjs "$trusted_sync_script" + trusted_sync_script="${RUNNER_TEMP}/sync-release-lockfile-versions.ts" + cp scripts/sync-release-lockfile-versions.ts "$trusted_sync_script" git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" gh auth setup-git branches="$(git ls-remote --heads origin 'release-please--branches--main--*' | awk '{print $2}' | sed 's#refs/heads/##')" @@ -137,7 +137,7 @@ jobs: for branch in $branches; do git fetch origin "$branch" git checkout -B "sync-check-$(printf '%s' "$branch" | tr '/' '-')" "origin/$branch" - node "$trusted_sync_script" packages/loopover-mcp packages/loopover-engine packages/loopover-miner packages/loopover-ui-kit + node --experimental-strip-types "$trusted_sync_script" packages/loopover-mcp packages/loopover-engine packages/loopover-miner packages/loopover-ui-kit if git diff --quiet package-lock.json; then echo "package-lock.json already in sync on $branch." else diff --git a/.github/workflows/mcp-release-watch.yml b/.github/workflows/mcp-release-watch.yml index bc85577afb..7bc031c553 100644 --- a/.github/workflows/mcp-release-watch.yml +++ b/.github/workflows/mcp-release-watch.yml @@ -24,15 +24,15 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version-file: .nvmrc + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Check MCP release status env: GITHUB_TOKEN: ${{ github.token }} - run: node scripts/check-mcp-release-due.mjs --json --output mcp-release-due.json --upsert-issue + # check-mcp-release-due.ts imports mcp-release-core.ts directly, so it needs tsx (not plain node) to + # resolve that local .ts import. + run: npx tsx scripts/check-mcp-release-due.ts --json --output mcp-release-due.json --upsert-issue - name: Summarize MCP release status run: | diff --git a/.github/workflows/mcp-ui-version-sync.yml b/.github/workflows/mcp-ui-version-sync.yml index 83f7c26975..34484d2add 100644 --- a/.github/workflows/mcp-ui-version-sync.yml +++ b/.github/workflows/mcp-ui-version-sync.yml @@ -1,7 +1,7 @@ # Keeps apps/loopover-ui/src/lib/mcp-package.ts's MCP_PACKAGE_KNOWN_LATEST_VERSION -- the client-side # graceful-degradation fallback shown when the live npm-registry fetch (useMcpPackageMetadata) fails or hasn't # resolved yet -- in sync with @loopover/mcp's real npm dist-tags.latest, without ever requiring a human/agent -# to notice a new release and manually edit the constant (#6580). scripts/check-ui-mcp-version-copy.mjs's +# to notice a new release and manually edit the constant (#6580). scripts/check-ui-mcp-version-copy.ts's # --write mode does the actual sync; this just runs it on a schedule and keeps a standing PR up to date, the # same shape as orb-stable-release-pr.yml. Nothing ships until a maintainer reviews and merges the PR -- this # workflow never pushes to main directly. @@ -38,7 +38,7 @@ jobs: # No npm ci -- the script only imports Node built-ins. - name: Sync the known-latest constant - run: node scripts/check-ui-mcp-version-copy.mjs --write + run: node --experimental-strip-types scripts/check-ui-mcp-version-copy.ts --write - name: Check for a diff id: diff diff --git a/.github/workflows/orb-beta-release.yml b/.github/workflows/orb-beta-release.yml index 98afa03598..af6630cfe8 100644 --- a/.github/workflows/orb-beta-release.yml +++ b/.github/workflows/orb-beta-release.yml @@ -1,6 +1,6 @@ # Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost) beta channel. # Daily (or on demand via workflow_dispatch), checks whether any image-relevant commit has landed -# since the last orb-v tag (scripts/check-orb-release-due.mjs / scripts/orb-release-core.mjs) and, +# since the last orb-v tag (scripts/check-orb-release-due.mjs / scripts/orb-release-core.ts) and, # if so, cuts the next `orb-vX.Y.Z-beta.N` tag and dispatches release-selfhost.yml to build + publish # it -- fully unattended: that workflow's `environment:` routes an actual beta version to # `release-beta` (no required reviewers), while a stable/rc version still requires the human-gated @@ -8,7 +8,7 @@ # a maintainer -- this workflow never bumps orb-manifest.json's version or cuts a non-beta tag. # # Deliberately independent of the MCP package's release automation (mcp-release-watch.yml / -# mcp-release-core.mjs) -- see scripts/orb-release-core.mjs's own header for why. +# mcp-release-core.ts) -- see scripts/orb-release-core.ts's own header for why. name: orb-beta-release on: @@ -35,16 +35,16 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version-file: .nvmrc + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Check whether an ORB beta is due id: report + # check-orb-release-due.mjs imports orb-release-core.ts directly, so it needs tsx (not plain node) to + # resolve that local .ts import. run: | set -euo pipefail - node scripts/check-orb-release-due.mjs --json --output orb-release-due.json + npx tsx scripts/check-orb-release-due.mjs --json --output orb-release-due.json node <<'NODE' const fs = require("node:fs"); const report = JSON.parse(fs.readFileSync("orb-release-due.json", "utf8")); @@ -60,7 +60,7 @@ jobs: # relying on this push alone. Mirrors publish-engine.yml / publish-mcp.yml's identical reasoning # and tagging idiom. # Exposes created=true/false so the dispatch step below never fires against a tag this run didn't - # actually just create -- a defense-in-depth backstop (independent of orb-release-core.mjs's own + # actually just create -- a defense-in-depth backstop (independent of orb-release-core.ts's own # correctness) against ever re-triggering a build for an already-published version/tag. - name: Tag the new beta id: tag diff --git a/.github/workflows/orb-stable-release-pr.yml b/.github/workflows/orb-stable-release-pr.yml index c63fb9b456..9ff1041133 100644 --- a/.github/workflows/orb-stable-release-pr.yml +++ b/.github/workflows/orb-stable-release-pr.yml @@ -1,10 +1,10 @@ # Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost) STABLE-channel Release PR -- # the release-please-equivalent for ORB, whose cross-cutting image-relevant scoping (src/** shared with -# UI/MCP-only subtrees it must exclude -- see orb-release-core.mjs's IMAGE_RELEVANT_PREFIXES/EXCLUDED_PREFIXES) +# UI/MCP-only subtrees it must exclude -- see orb-release-core.ts's IMAGE_RELEVANT_PREFIXES/EXCLUDED_PREFIXES) # doesn't fit release-please's directory-component model the way packages/loopover-mcp and # packages/loopover-engine do (see mcp-release-please.yml). Same UX contract as those, hand-rolled: on the # same schedule (or on demand), (re)compute the next stable version from conventional commits since the last -# STABLE orb-v tag (scripts/check-orb-stable-release-due.mjs / orb-release-core.mjs's buildOrbStableReleaseReport) +# STABLE orb-v tag (scripts/check-orb-stable-release-due.mjs / orb-release-core.ts's buildOrbStableReleaseReport) # and keep a standing `release-orb-stable` branch + PR in sync with that proposal. Nothing ships until a # maintainer reviews and merges it -- see orb-stable-release-tag.yml for what happens then. Never touches the # daily fully-unattended beta channel (orb-beta-release.yml). @@ -34,16 +34,16 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version-file: .nvmrc + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Check whether a stable ORB release is due id: report + # check-orb-stable-release-due.mjs imports orb-release-core.ts directly, so it needs tsx (not plain + # node) to resolve that local .ts import. run: | set -euo pipefail - node scripts/check-orb-stable-release-due.mjs --json --output orb-stable-release-due.json + npx tsx scripts/check-orb-stable-release-due.mjs --json --output orb-stable-release-due.json node <<'NODE' const fs = require("node:fs"); const report = JSON.parse(fs.readFileSync("orb-stable-release-due.json", "utf8")); diff --git a/.github/workflows/publish-mcp.yml b/.github/workflows/publish-mcp.yml index 1b37da4e4e..d5bc231290 100644 --- a/.github/workflows/publish-mcp.yml +++ b/.github/workflows/publish-mcp.yml @@ -253,7 +253,7 @@ jobs: GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} run: | set -euo pipefail - node scripts/check-ui-mcp-version-copy.mjs --write + node --experimental-strip-types scripts/check-ui-mcp-version-copy.ts --write if git diff --quiet apps/loopover-ui/src/lib/mcp-package.ts; then echo "Already in sync, nothing to do." exit 0 diff --git a/.github/workflows/publish-miner.yml b/.github/workflows/publish-miner.yml index 7b9742b169..5d0b9d625a 100644 --- a/.github/workflows/publish-miner.yml +++ b/.github/workflows/publish-miner.yml @@ -89,12 +89,10 @@ jobs: - name: Install dependencies run: npm ci - # Real tsc build (in-place emit -- see packages/loopover-miner/tsconfig.json), same as ci.yml's own - # "Build miner CLI" step, followed by node --check syntax validation over every bin/lib file (the - # same two-part script test:ci already runs on every PR). Re-running the compile here means this - # job always packs from a freshly-built tree even if the tagged commit's committed .js/.d.ts had - # somehow drifted from its .ts source (ci.yml's build-drift check on every PR is what keeps that - # from happening in the first place). + # Real tsc build (in-place emit -- see packages/loopover-miner/tsconfig.json; the emitted .js/.d.ts + # is gitignored, so this is the only place it ever exists), same as ci.yml's own "Build miner CLI" + # step, followed by node --check syntax validation over every bin/lib file (the same two-part + # script test:ci already runs on every PR). - name: Miner build + syntax validation run: npm run build --workspace @loopover/miner diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml index a5894ca415..fcc628304e 100644 --- a/.github/workflows/release-selfhost.yml +++ b/.github/workflows/release-selfhost.yml @@ -138,7 +138,7 @@ jobs: run: node scripts/build-selfhost.mjs --all - name: Validate release source map - run: node scripts/validate-selfhost-sourcemap.mjs + run: node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts - name: Detect Sentry release token id: sentry @@ -189,7 +189,7 @@ jobs: -d "$(node -e "process.stdout.write(JSON.stringify({commits:[{repository: process.env.SENTRY_REPOSITORY, id: process.env.SENTRY_COMMIT_SHA}]}))")" \ >/dev/null npx -y "$SENTRY_CLI_PACKAGE" sourcemaps inject dist - node scripts/validate-selfhost-sourcemap.mjs + node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts npx -y "$SENTRY_CLI_PACKAGE" sourcemaps upload --release="$SENTRY_RELEASE" --validate --wait --strict dist - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 diff --git a/.github/workflows/selfhost.yml b/.github/workflows/selfhost.yml index 843c005ed6..5b8c344809 100644 --- a/.github/workflows/selfhost.yml +++ b/.github/workflows/selfhost.yml @@ -12,7 +12,7 @@ on: - "src/selfhost/**" - "src/server.ts" - "scripts/build-selfhost.mjs" - - "scripts/validate-selfhost-sourcemap.mjs" + - "scripts/validate-selfhost-sourcemap.ts" - "Dockerfile" - "docker-compose.yml" - "migrations/**" @@ -28,7 +28,7 @@ on: - "src/selfhost/**" - "src/server.ts" - "scripts/build-selfhost.mjs" - - "scripts/validate-selfhost-sourcemap.mjs" + - "scripts/validate-selfhost-sourcemap.ts" - "Dockerfile" - "docker-compose.yml" - "migrations/**" @@ -100,7 +100,7 @@ jobs: run: node scripts/build-selfhost.mjs - name: Validate self-host source map - run: node scripts/validate-selfhost-sourcemap.mjs + run: node scripts/validate-selfhost-sourcemap.ts - name: Validate docker-compose.yml run: | diff --git a/.github/workflows/stuck-check-watchdog.yml b/.github/workflows/stuck-check-watchdog.yml index 109a058abc..f9bc222757 100644 --- a/.github/workflows/stuck-check-watchdog.yml +++ b/.github/workflows/stuck-check-watchdog.yml @@ -8,7 +8,7 @@ name: Stuck required-check watchdog # nothing surfacing that fact anywhere. This can't fix a stuck check (nothing on this repo's side can, # for a third-party check it doesn't control), it just makes the situation visible fast: flags any open # PR where a required check has been running past a threshold, via a single idempotent PR comment (see -# scripts/check-stuck-required-checks.mjs for the detection logic and why the required-check list is +# scripts/check-stuck-required-checks.ts for the detection logic and why the required-check list is # hardcoded rather than read live from branch protection). on: @@ -44,4 +44,4 @@ jobs: - name: Check for stuck required checks env: GITHUB_TOKEN: ${{ github.token }} - run: node scripts/check-stuck-required-checks.mjs${{ inputs.dry_run && ' --dry-run' || '' }} + run: node --experimental-strip-types scripts/check-stuck-required-checks.ts${{ inputs.dry_run && ' --dry-run' || '' }} diff --git a/.github/workflows/test-timing-refresh.yml b/.github/workflows/test-timing-refresh.yml index 996284468a..8d1446b2ad 100644 --- a/.github/workflows/test-timing-refresh.yml +++ b/.github/workflows/test-timing-refresh.yml @@ -1,7 +1,7 @@ name: Test timing refresh # Pulls per-test-file historical duration data from Codecov's Test Analytics API (see -# scripts/fetch-test-timing.mjs) and caches it for validate-tests' duration-aware shard bin-packer +# scripts/fetch-test-timing.ts) and caches it for validate-tests' duration-aware shard bin-packer # (scripts/compute-test-shards.mjs, ci.yml's "Test with coverage" step) to consume. Runs on a schedule # rather than per-PR: Codecov doesn't publish a numeric rate limit for this read endpoint, and this # repo's PR volume (hundreds/day) makes "query fresh on every PR" a real risk of hitting one, for data @@ -34,7 +34,7 @@ jobs: - name: Fetch test timing from Codecov env: CODECOV_API_TOKEN: ${{ secrets.CODECOV_API_TOKEN }} - run: node scripts/fetch-test-timing.mjs --output=test-timing.json + run: node --experimental-strip-types scripts/fetch-test-timing.ts --output=test-timing.json # Run_id-suffixed key (always a fresh entry, never a re-save of an existing one) + prefix # restore-keys fallback on the consuming side (ci.yml's "Restore test timing cache" step) -- same # accumulating-cache pattern already used for .turbo/cache and .tsbuildinfo elsewhere in this repo, diff --git a/.gitignore b/.gitignore index 1bbc3fe19b..c6529e1896 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,18 @@ 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 .turbo/ .output .vinxi diff --git a/Dockerfile b/Dockerfile index 0dc7e341c9..68f491cb82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN npm --workspace @loopover/engine run build # --all: bundle every dependency into one self-contained dist/server.mjs, so the runtime image needs no # node_modules (≈10× smaller). The bundle has zero `cloudflare:*` imports (stubbed at build), so no loader. RUN node scripts/build-selfhost.mjs --all -RUN node scripts/validate-selfhost-sourcemap.mjs +RUN node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts # --- runtime base: slim, non-root ----------------------------------------------------------------------- FROM public.ecr.aws/docker/library/node:24-slim AS runtime-base diff --git a/apps/loopover-extension/package.json b/apps/loopover-extension/package.json index dc29d3d2f2..260f2e5785 100644 --- a/apps/loopover-extension/package.json +++ b/apps/loopover-extension/package.json @@ -5,7 +5,7 @@ "description": "Maintainer-facing browser extension overlay for GitHub pull requests and issues.", "type": "module", "scripts": { - "build": "node ../../scripts/build-extension.mjs", + "build": "tsx ../../scripts/build-extension.ts", "lint": "node --check auth.js && node --check background.js && node --check content.js && node --check options.js", "typecheck": "npm run lint" } diff --git a/apps/loopover-miner-extension/package.json b/apps/loopover-miner-extension/package.json index 30378a94d8..47596e200f 100644 --- a/apps/loopover-miner-extension/package.json +++ b/apps/loopover-miner-extension/package.json @@ -5,7 +5,7 @@ "description": "Contributor-facing browser extension for GitHub issue opportunity signals.", "type": "module", "scripts": { - "build": "node ../../scripts/build-miner-extension.mjs", + "build": "tsx ../../scripts/build-miner-extension.ts", "lint": "node --check background.js && node --check content.js && node --check opportunity-badge.js && node --check options.js && node --check toolbar-badge.js", "typecheck": "npm run lint", "test": "vitest run --coverage" diff --git a/apps/loopover-ui/src/lib/ams-env-reference.ts b/apps/loopover-ui/src/lib/ams-env-reference.ts index bcca7cfd6d..72c8d25fe1 100644 --- a/apps/loopover-ui/src/lib/ams-env-reference.ts +++ b/apps/loopover-ui/src/lib/ams-env-reference.ts @@ -8,177 +8,177 @@ export type MinerEnvReferenceRow = { export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ { name: "LOOPOVER_MINER_AMS_COLLECTOR_TOKEN", - firstReference: "lib/orb-export.js", + firstReference: "lib/orb-export.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_AMS_COLLECTOR_URL", - firstReference: "lib/orb-export.js", + firstReference: "lib/orb-export.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID", - firstReference: "lib/oauth-device-flow.js", + firstReference: "lib/oauth-device-flow.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_AMS_POLICY_PATH", - firstReference: "lib/ams-policy.js", + firstReference: "lib/ams-policy.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_ATTEMPT_LOG_DB", - firstReference: "lib/attempt-log.js", + firstReference: "lib/attempt-log.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_CLAIM_LEDGER_DB", - firstReference: "lib/claim-ledger.js", + firstReference: "lib/claim-ledger.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_CONFIG_DIR", - firstReference: "lib/deny-hook-synthesis.js", + firstReference: "lib/deny-hook-synthesis.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB", - firstReference: "lib/contribution-profile-cache.js", + firstReference: "lib/contribution-profile-cache.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_CONTROL_PLANE", - firstReference: "lib/tenant-client.js", + firstReference: "lib/tenant-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN", - firstReference: "lib/tenant-client.js", + firstReference: "lib/tenant-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_CONTROL_PLANE_URL", - firstReference: "lib/tenant-client.js", + firstReference: "lib/tenant-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", - firstReference: "lib/deny-hook-synthesis.js", + firstReference: "lib/deny-hook-synthesis.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_DISCOVERY_INDEX_URL", - firstReference: "lib/discovery-index-client.js", + firstReference: "lib/discovery-index-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_DISCOVERY_PLANE", - firstReference: "lib/discovery-index-client.js", + firstReference: "lib/discovery-index-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_DISCOVERY_SHARED_SECRET", - firstReference: "lib/discovery-index-client.js", + firstReference: "lib/discovery-index-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_DISCOVERY_TELEMETRY", - firstReference: "lib/discovery-index-client.js", + firstReference: "lib/discovery-index-client.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_EVENT_LEDGER_DB", - firstReference: "lib/event-ledger.js", + firstReference: "lib/event-ledger.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_GOVERNOR_LEDGER_DB", - firstReference: "lib/governor-ledger.js", + firstReference: "lib/governor-ledger.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_GOVERNOR_STATE_DB", - firstReference: "lib/governor-state.js", + firstReference: "lib/governor-state.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_LOG_LEVEL", - firstReference: "lib/logger.js", + firstReference: "lib/logger.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_NO_UPDATE_CHECK", - firstReference: "lib/update-check.js", + firstReference: "lib/update-check.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_ORB_EXPORT_DB", - firstReference: "lib/orb-export.js", + firstReference: "lib/orb-export.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_PLAN_STORE_DB", - firstReference: "lib/plan-store.js", + firstReference: "lib/plan-store.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_POLICY_DOC_CACHE_DB", - firstReference: "lib/policy-doc-cache.js", + firstReference: "lib/policy-doc-cache.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB", - firstReference: "lib/policy-verdict-cache.js", + firstReference: "lib/policy-verdict-cache.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_PORTFOLIO_QUEUE_DB", - firstReference: "lib/portfolio-queue.js", + firstReference: "lib/portfolio-queue.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_PREDICTION_LEDGER_DB", - firstReference: "lib/prediction-ledger.js", + firstReference: "lib/prediction-ledger.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_RANKED_CANDIDATES_DB", - firstReference: "lib/ranked-candidates.js", + firstReference: "lib/ranked-candidates.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_REPLAY_SNAPSHOT_DB", - firstReference: "lib/replay-snapshot.js", + firstReference: "lib/replay-snapshot.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_RUN_STATE_DB", - firstReference: "lib/run-state.js", + firstReference: "lib/run-state.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_SENTRY_DSN", - firstReference: "lib/sentry.js", + firstReference: "lib/sentry.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_SENTRY_ENVIRONMENT", - firstReference: "lib/sentry.js", + firstReference: "lib/sentry.ts", defaultValue: "production", }, { name: "LOOPOVER_MINER_VERSION", - firstReference: "lib/version.js", + firstReference: "lib/version.ts", defaultValue: "", }, { name: "LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB", - firstReference: "lib/worktree-allocator.js", + firstReference: "lib/worktree-allocator.ts", defaultValue: null, }, { name: "LOOPOVER_MINER_WORKTREE_DIR", - firstReference: "lib/worktree-allocator.js", + firstReference: "lib/worktree-allocator.ts", defaultValue: "", }, { @@ -198,7 +198,7 @@ export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ }, { name: "MINER_CODING_AGENT_PROVIDER", - firstReference: "lib/laptop-init.js", + firstReference: "lib/laptop-init.ts", defaultValue: "", }, { @@ -215,45 +215,45 @@ export const AMS_ENV_REFERENCE_MARKDOWN = [ "", "| Name | First reference | Default |", "| --- | --- | --- |", - '| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.js` | `""` |', - '| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.js` | `""` |', - '| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.js` | `""` |', - "| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.js` | (none) |", - "| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.js` | (none) |", - "| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.js` | (none) |", - '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.js` | `""` |', - "| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.js` | (none) |", - '| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.js` | `""` |', - '| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.js` | `""` |', - '| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.js` | `""` |', - '| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.js` | `""` |', - '| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.js` | `""` |', - '| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.js` | `""` |', - '| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `lib/discovery-index-client.js` | `""` |', - '| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `lib/discovery-index-client.js` | `""` |', - "| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.js` | (none) |", - "| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | (none) |", - "| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) |", - '| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.js` | `""` |', - '| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` |', - '| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` |', - "| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | (none) |", - "| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.js` | (none) |", - "| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.js` | (none) |", - "| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.js` | (none) |", - "| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.js` | (none) |", - "| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.js` | (none) |", - "| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.js` | (none) |", - "| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.js` | (none) |", - "| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.js` | (none) |", - '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `"production"` |', - '| `LOOPOVER_MINER_VERSION` | `lib/version.js` | `""` |', - "| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.js` | (none) |", - '| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.js` | `""` |', + '| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.ts` | `""` |', + '| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.ts` | `""` |', + '| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.ts` | `""` |', + "| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) |", + "| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) |", + "| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) |", + '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` |', + "| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) |", + '| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` |', + '| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` |', + '| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` |', + '| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` |', + '| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` |', + '| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` |', + '| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `lib/discovery-index-client.ts` | `""` |', + '| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `lib/discovery-index-client.ts` | `""` |', + "| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) |", + "| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) |", + "| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) |", + '| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` |', + '| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` |', + '| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` |', + "| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) |", + "| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) |", + "| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) |", + "| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.ts` | (none) |", + "| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.ts` | (none) |", + "| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.ts` | (none) |", + "| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.ts` | (none) |", + "| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.ts` | (none) |", + "| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.ts` | (none) |", + '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.ts` | `"production"` |', + '| `LOOPOVER_MINER_VERSION` | `lib/version.ts` | `""` |', + "| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.ts` | (none) |", + '| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.ts` | `""` |', "| `MINER_CODING_AGENT_CLAUDE_MODEL` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) |", "| `MINER_CODING_AGENT_CODEX_MODEL` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) |", '| `MINER_CODING_AGENT_PAUSED` | `packages/loopover-engine/src/miner/coding-agent-mode.ts` | `""` |', - '| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.js` | `""` |', + '| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.ts` | `""` |', "| `MINER_CODING_AGENT_TIMEOUT_MS` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) |", "", ].join("\n"); diff --git a/apps/loopover-ui/src/lib/command-reference.ts b/apps/loopover-ui/src/lib/command-reference.ts index 01ec44a5c3..23ad404388 100644 --- a/apps/loopover-ui/src/lib/command-reference.ts +++ b/apps/loopover-ui/src/lib/command-reference.ts @@ -1,4 +1,4 @@ -// Generated by scripts/gen-command-reference.mjs. Do not edit manually. +// Generated by scripts/gen-command-reference.ts. Do not edit manually. // Regenerate via `npm run command-reference`. export const PUBLIC_COMMAND_ENTRIES = [ { diff --git a/apps/loopover-ui/src/lib/selfhost-docs-audit.ts b/apps/loopover-ui/src/lib/selfhost-docs-audit.ts index 09e72aafa5..ceeab4a3ec 100644 --- a/apps/loopover-ui/src/lib/selfhost-docs-audit.ts +++ b/apps/loopover-ui/src/lib/selfhost-docs-audit.ts @@ -103,7 +103,7 @@ export const SELFHOST_SOURCE_OF_TRUTH_ROWS: readonly SelfHostSourceOfTruthRow[] { topic: "Env vars (exhaustive list)", runtimeSources: [ - "scripts/gen-selfhost-env-reference.mjs", + "scripts/gen-selfhost-env-reference.ts", "apps/loopover-ui/src/lib/selfhost-env-reference.ts", ], docsPath: "/docs/self-hosting-configuration", @@ -187,7 +187,7 @@ export const SELFHOST_SOURCE_OF_TRUTH_ROWS: readonly SelfHostSourceOfTruthRow[] topic: "OTEL metrics/traces and Grafana dashboards", runtimeSources: [ "src/selfhost/otel.ts", - "scripts/validate-observability-configs.mjs", + "scripts/validate-observability-configs.ts", "grafana/dashboards/", "prometheus/rules/alerts.yml", ], diff --git a/apps/loopover-ui/src/lib/selfhost-env-reference.ts b/apps/loopover-ui/src/lib/selfhost-env-reference.ts index d41d76c840..d5fefd140a 100644 --- a/apps/loopover-ui/src/lib/selfhost-env-reference.ts +++ b/apps/loopover-ui/src/lib/selfhost-env-reference.ts @@ -1,4 +1,4 @@ -// Generated by scripts/gen-selfhost-env-reference.mjs. Do not edit manually. +// Generated by scripts/gen-selfhost-env-reference.ts. Do not edit manually. export type SelfHostEnvReferenceRow = { name: string; firstReference: string; diff --git a/docs/load-test-worker.md b/docs/load-test-worker.md index bb2baf2730..b729312ef9 100644 --- a/docs/load-test-worker.md +++ b/docs/load-test-worker.md @@ -22,7 +22,7 @@ Then, in another terminal: ```sh npm run loadtest:worker # or with explicit flags: -node scripts/load-test-worker.mjs --origin http://127.0.0.1:8787 --path /health --levels 1,8,32,128 --requests-per-level 64 +node --experimental-strip-types scripts/load-test-worker.ts --origin http://127.0.0.1:8787 --path /health --levels 1,8,32,128 --requests-per-level 64 ``` This prints a short text report to stdout and exits `0`. It does not fail the build or a CI job on its diff --git a/orb-manifest.json b/orb-manifest.json index e34ce7c06c..b28e2f501c 100644 --- a/orb-manifest.json +++ b/orb-manifest.json @@ -1,4 +1,4 @@ { "version": "3.2.0", - "description": "Source of truth for the self-hostable loopover-orb container image's target release version (ghcr.io/jsonbored/loopover-selfhost). Bumped by a maintainer when a feat/fix/breaking change since the last stable orb-v tag warrants moving to a new target version -- scripts/orb-release-core.mjs and .github/workflows/orb-beta-release.yml read this file to decide what version the next automated beta snapshot targets. Promoting a beta to a stable orb-vX.Y.Z release is still a manual `git tag` -- this manifest only drives the automated beta channel." + "description": "Source of truth for the self-hostable loopover-orb container image's target release version (ghcr.io/jsonbored/loopover-selfhost). Bumped by a maintainer when a feat/fix/breaking change since the last stable orb-v tag warrants moving to a new target version -- scripts/orb-release-core.ts and .github/workflows/orb-beta-release.yml read this file to decide what version the next automated beta snapshot targets. Promoting a beta to a stable orb-vX.Y.Z release is still a manual `git tag` -- this manifest only drives the automated beta channel." } diff --git a/package-lock.json b/package-lock.json index 08ce6b6c3c..f26ec6ab53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@types/pg": "^8.20.0", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", + "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.1.9", "ajv": "^8.20.0", "drizzle-kit": "^0.31.10", @@ -8506,6 +8507,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", diff --git a/package.json b/package.json index ba027784de..d784f16654 100644 --- a/package.json +++ b/package.json @@ -15,30 +15,28 @@ "deploy": "wrangler deploy", "deploy:api": "npm --workspace @loopover/engine run build && wrangler d1 migrations apply loopover --remote && wrangler deploy", "selfhost:postgres:migrate": "tsx scripts/migrate-selfhost-sqlite-to-postgres.ts", - "selfhost:env-reference": "node scripts/gen-selfhost-env-reference.mjs", - "selfhost:env-reference:check": "node scripts/gen-selfhost-env-reference.mjs --check", - "miner:env-reference": "node packages/loopover-miner/scripts/generate-env-reference.mjs", - "miner:env-reference:check": "node packages/loopover-miner/scripts/generate-env-reference.mjs --check", + "selfhost:env-reference": "node --experimental-strip-types scripts/gen-selfhost-env-reference.ts", + "selfhost:env-reference:check": "node --experimental-strip-types scripts/gen-selfhost-env-reference.ts --check", + "miner:env-reference": "tsx packages/loopover-miner/scripts/generate-env-reference.mjs", + "miner:env-reference:check": "tsx packages/loopover-miner/scripts/generate-env-reference.mjs --check", "benchmark:miner": "node packages/loopover-miner/scripts/benchmark.mjs", "cross-repo-eval:miner": "node packages/loopover-miner/scripts/cross-repo-evaluation.mjs", "loadtest:iterate-loop": "npm run build --workspace @loopover/engine && node packages/loopover-engine/scripts/load-test-iterate-loop.mjs", - "loadtest:worker": "node scripts/load-test-worker.mjs", - "command-reference": "node scripts/gen-command-reference.mjs", - "command-reference:check": "node scripts/gen-command-reference.mjs --check", - "selfhost:validate-observability": "node scripts/validate-observability-configs.mjs", + "loadtest:worker": "node --experimental-strip-types scripts/load-test-worker.ts", + "command-reference": "node --experimental-strip-types scripts/gen-command-reference.ts", + "command-reference:check": "node --experimental-strip-types scripts/gen-command-reference.ts --check", + "selfhost:validate-observability": "node --experimental-strip-types scripts/validate-observability-configs.ts", "selfhost:config-lint": "tsx scripts/loopover-config-lint.ts", - "cf-typegen": "node scripts/gen-cf-typegen.mjs", - "cf-typegen:check": "node scripts/gen-cf-typegen.mjs --check", + "cf-typegen": "node --experimental-strip-types scripts/gen-cf-typegen.ts", + "cf-typegen:check": "node --experimental-strip-types scripts/gen-cf-typegen.ts --check", "db:migrate:local": "wrangler d1 migrations apply loopover --local", "db:migrate:remote": "wrangler d1 migrations apply loopover --remote", "drizzle:generate": "drizzle-kit generate", "build:mcp": "npm --workspace @loopover/mcp run build", - "build:mcp:check": "npm run build:mcp && node scripts/check-build-drift.mjs mcp", "build:miner": "turbo run build --filter=@loopover/engine && npm --workspace @loopover/miner run build", - "build:miner:check": "npm run build:miner && node scripts/check-build-drift.mjs miner", - "test:mcp-pack": "node scripts/check-mcp-package.mjs", - "test:miner-pack": "node scripts/check-miner-package.mjs", - "test:miner-deployment-docs-audit": "node scripts/check-miner-deployment-docs.mjs", + "test:mcp-pack": "tsx scripts/check-mcp-package.mjs", + "test:miner-pack": "tsx scripts/check-miner-package.mjs", + "test:miner-deployment-docs-audit": "tsx scripts/check-miner-deployment-docs.ts", "rees:install": "npm ci --prefix review-enrichment --prefer-offline --no-audit --no-fund", "rees:test": "npm run rees:install && npm --prefix review-enrichment test", "rees:metadata": "npm --prefix review-enrichment run metadata", @@ -49,11 +47,11 @@ "control-plane:test": "npm run control-plane:install && npm --prefix control-plane test", "control-plane:coverage": "node scripts/control-plane-coverage.mjs", "db:migrations:check": "tsx scripts/check-migrations.mjs", - "db:schema-drift:check": "tsx scripts/check-schema-drift.mjs", + "db:schema-drift:check": "tsx scripts/check-schema-drift.ts", "actionlint": "node scripts/actionlint.mjs", - "lint:composite-actions": "node scripts/lint-composite-actions.mjs", + "lint:composite-actions": "node --experimental-strip-types scripts/lint-composite-actions.ts", "ui:dev": "npm run ui:preview", - "extension:build": "node scripts/build-extension.mjs", + "extension:build": "tsx scripts/build-extension.ts", "extension:lint": "npm --workspace @loopover/extension run lint", "extension:typecheck": "npm --workspace @loopover/extension run typecheck", "miner-extension:build": "npm --workspace @loopover/miner-extension run build", @@ -68,31 +66,31 @@ "ui:test": "npm run ui:kit:build && npm --workspace @loopover/ui run test && npm --workspace @loopover/ui-miner run test && npm --workspace @loopover/miner-extension run test", "ui:openapi": "tsx scripts/write-ui-openapi.ts", "ui:openapi:check": "tsx scripts/write-ui-openapi.ts --check", - "ui:openapi:settings-parity": "tsx scripts/check-openapi-settings-parity.mjs", + "ui:openapi:settings-parity": "tsx scripts/check-openapi-settings-parity.ts", "cloudflare:schema": "tsx scripts/write-cloudflare-schema.ts", - "ui:version-audit": "node scripts/check-ui-mcp-version-copy.mjs", - "ui:version-audit:sync": "node scripts/check-ui-mcp-version-copy.mjs --write", - "docs:drift-check": "node scripts/check-docs-drift.mjs", - "branding-drift:check": "node scripts/check-branding-drift.mjs", - "branding-drift:update": "node scripts/check-branding-drift.mjs --update", - "manifest:drift-check": "tsx scripts/check-manifest-drift.mjs", + "ui:version-audit": "node --experimental-strip-types scripts/check-ui-mcp-version-copy.ts", + "ui:version-audit:sync": "node --experimental-strip-types scripts/check-ui-mcp-version-copy.ts --write", + "docs:drift-check": "node --experimental-strip-types scripts/check-docs-drift.ts", + "branding-drift:check": "node --experimental-strip-types scripts/check-branding-drift.ts", + "branding-drift:update": "node --experimental-strip-types scripts/check-branding-drift.ts --update", + "manifest:drift-check": "tsx scripts/check-manifest-drift.ts", "engine-parity:drift-check": "tsx scripts/check-engine-parity.ts", - "engines-nvmrc:check": "node scripts/check-engines-nvmrc-sync.mjs", - "release-manifest:sync": "node scripts/sync-release-manifest.mjs", - "release-manifest:sync:check": "node scripts/sync-release-manifest.mjs --check", + "engines-nvmrc:check": "node --experimental-strip-types scripts/check-engines-nvmrc-sync.ts", + "release-manifest:sync": "node --experimental-strip-types scripts/sync-release-manifest.ts", + "release-manifest:sync:check": "node --experimental-strip-types scripts/sync-release-manifest.ts --check", "ui:deploy": "npm run ui:build && npm run ui:deploy:built", "ui:deploy:built": "wrangler deploy --config apps/loopover-ui/dist/server/wrangler.json", "ui:version:built": "wrangler versions upload --config apps/loopover-ui/dist/server/wrangler.json", "changelog": "npm run changelog:root && npm run changelog:mcp", "changelog:root": "git-cliff --config cliff.toml --output CHANGELOG.md", - "changelog:mcp": "node scripts/generate-mcp-changelog.mjs --output packages/loopover-mcp/CHANGELOG.md", + "changelog:mcp": "tsx scripts/generate-mcp-changelog.ts --output packages/loopover-mcp/CHANGELOG.md", "changelog:check": "node scripts/check-changelog.mjs", "changelog:check:root": "node scripts/check-changelog.mjs --root", "changelog:check:mcp": "node scripts/check-changelog.mjs --mcp", - "mcp:release-due": "node scripts/check-mcp-release-due.mjs --json", - "mcp:release-candidate": "node scripts/check-mcp-release-candidate.mjs", + "mcp:release-due": "tsx scripts/check-mcp-release-due.ts --json", + "mcp:release-candidate": "tsx scripts/check-mcp-release-candidate.mjs", "typecheck": "tsc --noEmit", - "check-node-version": "node scripts/check-node-version.mjs", + "check-node-version": "node --experimental-strip-types scripts/check-node-version.ts", "pretest": "npm run check-node-version", "test": "vitest run", "test:unit": "vitest run test/unit", @@ -106,13 +104,13 @@ "test:workers": "vitest run --config vitest.workers.config.ts", "pretest:coverage": "npm run check-node-version", "test:coverage": "vitest run --coverage --pool=forks", - "test:smoke:production": "node scripts/smoke-production.mjs", + "test:smoke:production": "tsx scripts/smoke-production.ts", "test:smoke:observability": "node scripts/smoke-observability-traces.mjs", "test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs", "test:smoke:browser:install": "playwright install chromium", "test:smoke:browser": "node scripts/smoke-ui-browser.mjs", "pretest:ci": "npm run check-node-version", - "test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build:mcp:check && npm run build:miner:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", + "test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", "test:release:mcp": "npm run test:ci", "test:watch": "vitest", @@ -151,6 +149,7 @@ "@types/pg": "^8.20.0", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", + "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.1.9", "ajv": "^8.20.0", "drizzle-kit": "^0.31.10", @@ -189,6 +188,7 @@ }, "main": "index.js", "directories": { + "doc": "docs", "test": "test" }, "repository": { diff --git a/packages/loopover-mcp/bin/loopover-mcp.js b/packages/loopover-mcp/bin/loopover-mcp.js deleted file mode 100755 index 3d73fbb5e0..0000000000 --- a/packages/loopover-mcp/bin/loopover-mcp.js +++ /dev/null @@ -1,6028 +0,0 @@ -#!/usr/bin/env node -// Phase 3 of #7291 / #7330 — design decision: single-file 1:1 JS→TS conversion (not a split). -// Seams exist, but splitting would be a separate refactor; this PR only routes the CLI through tsc. -import { createHash } from "node:crypto"; -import { closeSync, constants as fsConstants, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { delimiter, dirname, join } from "node:path"; -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"; -// #6149: the miner write-tools are PURE local-execution spec builders (loopover never performs the write); -// registering them locally is just importing the same engine builders the remote server uses. -import { buildApplyLabelsSpec, buildClosePrSpec, buildCreateBranchSpec, buildDeleteBranchSpec, buildFileIssueSpec, buildFollowUpIssueSpec, buildOpenPrSpec, buildPostEligibilityCommentSpec, buildTestGenSpec, } from "@loopover/engine"; -// #6269: the same manifest-validation builder the remote server uses, so `loopover_validate_config` -// can validate a `.loopover.yml` in-process instead of round-tripping to the API. -import { buildFocusManifestValidation } from "@loopover/engine"; -// #6150: the same deterministic token-score computation the remote server's loopover_run_local_scorer -// wraps, so it works fully offline here too. -import { computeLocalScorerTokens } from "@loopover/engine"; -import { buildSlopAssessment, SLOP_RUBRIC_MARKDOWN } from "@loopover/engine/signals/slop"; -// #6749: the same pure builder the remote MCP tool + /v1/lint/test-evidence both call. -import { buildTestEvidenceReport } from "@loopover/engine/signals/test-evidence"; -// #6754: the same pure evaluator the remote MCP tool + /v1/loop/evaluate-escalation both call. -import { evaluateEscalation } from "@loopover/engine"; -// #6752: the same pure composer the remote MCP tool + /v1/loop/results-payload both call. -import { buildResultsPayload } from "@loopover/engine"; -// #6753: the same pure composer the remote MCP tool + /v1/loop/progress-snapshot both call. -import { buildProgressSnapshot } from "@loopover/engine"; -// #6755: the same pure bridge the remote MCP tool + /v1/loop/intake-idea both call. -import { validateIdeaSubmission, buildTaskGraph, buildClaimPlan } from "@loopover/engine"; -import { z } from "zod"; -import { buildBranchAnalysisPayload, collectLocalDiff, collectLocalBranchMetadata, probeLocalScorer, referenceScorePreviewExample, resolveScorePreviewCommand, resolveWorkspaceCwd, sanitizeLocalScorerStatus, setupGuidanceForLocalScorer, isTestFile } from "../lib/local-branch.js"; -import { formatTable } from "../lib/format-table.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "../lib/cli-error.js"; -import { redactKnownLocalPaths, redactLocalPath } from "../lib/redact-local-path.js"; -// Aliased: this file's own recordStdioToolTelemetry is the chokepoint that calls it, and the two names sitting -// side by side unaliased would read as the same function (#6238). -import { recordMcpToolCall as recordLocalMcpToolCall } from "../lib/telemetry.js"; -// 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 defaultApiUrl = "https://api.loopover.ai"; -const legacyDefaultApiUrls = new Set([ - "https://gittensory-api.zeronode.workers.dev", - "https://gittensory-api.aethereal.dev", -]); -const packageName = ownPackageJson.name; -const packageVersion = ownPackageJson.version; -const npmRegistryUrl = (process.env.LOOPOVER_NPM_REGISTRY_URL ?? "https://registry.npmjs.org").replace(/\/+$/, ""); -const upgradeCommand = `npm install -g ${packageName}@latest`; -const npxFallbackCommand = `npx ${packageName}@latest `; -const compatibilityPath = "/v1/mcp/compatibility"; -const findingTaxonomyPath = "/v1/mcp/finding-taxonomy"; -const enrichmentAnalyzersPath = "/v1/mcp/enrichment-analyzers"; -const currentApiVersion = "0.1.0"; -const decisionPackCacheSchemaVersion = 1; -const decisionPackCacheMaxEntries = 25; -const decisionPackCacheMaxBytes = 512 * 1024; -const cliTextFileMaxBytes = 1024 * 1024; -const changelogPath = new URL("../CHANGELOG.md", import.meta.url); -const cliArgs = process.argv.slice(2); -const defaultProfileName = "default"; -// Single source of truth for shell-completion: top-level command -> its subcommands (if any). -const CLI_COMMAND_SPEC = { - login: [], - logout: [], - whoami: [], - config: [], - status: [], - changelog: [], - completion: [], - version: [], - tools: ["search"], - doctor: [], - telemetry: ["enable", "disable", "status"], - "init-client": [], - "decision-pack": [], - "repo-decision": [], - "contributor-profile": [], - "monitor-open-prs": [], - "pr-outcomes": [], - "explain-review-risk": [], - notifications: [], - "notifications-read": [], - watch: ["list", "add", "remove"], - "analyze-branch": [], - preflight: [], - "review-pr": [], - "lint-pr-text": [], - "validate-config": [], - "slop-risk": [], - "improvement-potential": [], - "issue-slop": [], - profile: ["list", "create", "switch", "remove"], - cache: ["status", "clear", "list"], - agent: ["plan", "status", "explain", "packet"], - maintain: ["status", "queue", "propose", "approve", "reject", "pause", "resume", "set-level", "precision", "outcome-calibration", "onboarding-pack", "audit-feed", "automation-state", "refresh-docs", "generate-issue-drafts"], -}; -const COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell"]; -const AGENT_PROFILE_IDS = ["miner-planner", "miner-auto-dev", "maintainer-triage", "repo-owner-intake"]; -// #784 maintain set-level — the autonomy dial's action classes + levels. -// -// Both are hand-synced literals, not imports: this file resolves @loopover/engine through the PUBLISHED package -// (`^3.0.0`), whose export map exposes only `.` + a few `./scoring/*`/`./signals/*` subpaths — neither surfaces -// AUTONOMY_LEVELS, so importing the canonical list would mean widening the engine's public API (#6153). The -// drift this invites is real and has bitten once already, so test/unit/mcp-cli-maintain.test.ts pins LEVELS -// against the live enum and fails the moment the two disagree. -// -// LEVELS mirrors AUTONOMY_LEVELS (src/settings/autonomy.ts -> packages/loopover-engine/src/settings/autonomy.ts) -// exactly. #6153: it carried "suggest"/"propose" for the whole life of #4620, which dropped them server-side -- -// PUT /settings validates against the live enum (src/api/routes.ts), so every value this list accepted but the -// server didn't turned an immediate, clear client-side error into a confusing 400 from the API. -// -// ACTION_CLASSES is deliberately NOT the engine's full AGENT_ACTION_CLASSES: it is the operator-settable subset -// the maintain surface exposes, and src/mcp/server.ts's MAINTAIN_AUTONOMY_ACTION_CLASSES mirrors these six on -// purpose. Do not "sync" it to the engine list. -const MAINTAIN_ACTION_CLASSES = ["review", "request_changes", "approve", "merge", "close", "label"]; -const MAINTAIN_AUTONOMY_LEVELS = ["observe", "auto_with_approval", "auto"]; -// #6744: the loopover_propose_action / POST .../agent/pending-actions action-class enum. A superset of -// MAINTAIN_ACTION_CLASSES (adds review_state_label) — kept separate so `maintain propose` accepts exactly what the -// route + MCP tool accept, while set-level keeps its own autonomy-configurable subset above. -const PROPOSE_ACTION_CLASSES = ["review", "request_changes", "approve", "merge", "close", "label", "review_state_label"]; -// #6150 — plan-DAG step tracking for loopover_build_plan/loopover_plan_status/loopover_record_step_result. -// Hand-duplicated from src/services/plan-dag.ts (packages/loopover-engine/src/services/plan-dag.ts is NOT -// where it lives -- this module was never extracted to @loopover/engine, so there is nothing to import from -// the published package's export map), same rationale as MAINTAIN_ACTION_CLASSES/AUTONOMY_LEVELS above: this -// file resolves @loopover/engine through the published package, whose export map does not surface it. -// PURE + stateless (no DB, no repo/network access) -- the harness performs each step's real work and calls -// loopover_record_step_result to report it back; this only advances the in-memory state machine the caller -// passes in and gets back on every call. -const DEFAULT_PLAN_MAX_ATTEMPTS = 1; -function buildPlanDag(steps) { - return { - steps: steps.map((step) => ({ - id: step.id, - title: step.title, - ...(step.actionClass !== undefined ? { actionClass: step.actionClass } : {}), - dependsOn: [...new Set((step.dependsOn ?? []).filter((dep) => dep !== step.id))], - status: "pending", - attempts: 0, - maxAttempts: Math.min(10, Math.max(1, Math.trunc(step.maxAttempts ?? DEFAULT_PLAN_MAX_ATTEMPTS))), - })), - }; -} -function validatePlanDag(plan) { - const errors = []; - const ids = plan.steps.map((step) => step.id); - const idSet = new Set(ids); - if (idSet.size !== ids.length) - errors.push("duplicate step ids"); - for (const step of plan.steps) { - for (const dep of step.dependsOn) { - if (!idSet.has(dep)) - errors.push(`step ${step.id} depends on unknown step ${dep}`); - } - } - const color = new Map(); - const byId = new Map(plan.steps.map((step) => [step.id, step])); - const hasCycle = (id) => { - color.set(id, 1); - for (const dep of byId.get(id)?.dependsOn ?? []) { - const depColor = color.get(dep) ?? 0; - if (depColor === 1) - return true; - if (depColor === 0 && byId.has(dep) && hasCycle(dep)) - return true; - } - color.set(id, 2); - return false; - }; - for (const step of plan.steps) { - if ((color.get(step.id) ?? 0) === 0 && hasCycle(step.id)) { - errors.push("plan has a dependency cycle"); - break; - } - } - return { valid: errors.length === 0, errors }; -} -const isPlanStepDone = (status) => status === "completed" || status === "skipped"; -function nextReadySteps(plan) { - const statusById = new Map(plan.steps.map((step) => [step.id, step.status])); - return plan.steps.filter((step) => step.status === "pending" && step.dependsOn.every((dep) => isPlanStepDone(statusById.get(dep) ?? "pending"))); -} -function mapPlanStep(plan, stepId, update) { - return { steps: plan.steps.map((step) => (step.id === stepId ? update(step) : step)) }; -} -function applyStepResult(plan, stepId, result) { - return mapPlanStep(plan, stepId, (step) => { - if (isPlanStepDone(step.status) || step.status === "failed") - return step; - if (result.outcome === "completed") - return { ...step, status: "completed", lastError: null }; - if (result.outcome === "skipped") - return { ...step, status: "skipped", lastError: null }; - const attempts = step.attempts + 1; - const exhausted = attempts >= step.maxAttempts; - return { ...step, attempts, status: exhausted ? "failed" : "pending", lastError: result.error ?? "step failed" }; - }); -} -function planProgress(plan) { - const count = (status) => plan.steps.filter((step) => step.status === status).length; - const completed = count("completed"); - const skipped = count("skipped"); - const failed = count("failed"); - const running = count("running"); - const pending = count("pending"); - const total = plan.steps.length; - let status; - if (total > 0 && completed + skipped === total) - status = "completed"; - else if (failed > 0) - status = "failed"; - else if (running > 0) - status = "running"; - else if (pending > 0 && nextReadySteps(plan).length === 0) - status = "blocked"; - else - status = "pending"; - return { total, completed, failed, running, pending, skipped, status }; -} -function planView(plan) { - return { - plan, - progress: planProgress(plan), - readySteps: nextReadySteps(plan).map((step) => ({ id: step.id, title: step.title })), - validation: validatePlanDag(plan), - }; -} -const AGENT_PROFILES = { - "miner-planner": { - id: "miner-planner", - title: "Miner planner", - audience: "contributors choosing and preparing Gittensor OSS work", - purpose: "Plan cleanup-first work, run branch preflight, explain blockers, and prepare public-safe PR packets.", - recommendedPrompts: ["loopover_miner_select_issue", "loopover_miner_branch_preflight", "loopover_miner_cleanup_first", "loopover_miner_draft_pr_packet"], - recommendedTools: ["loopover_agent_plan_next_work", "loopover_preflight_current_branch", "loopover_agent_prepare_pr_packet"], - boundaries: [ - "Human-approved only: plan, explain, draft, and prepare packets; do not open PRs, post comments, label, close, merge, or publish public GitHub output.", - "Use public-safe summaries for copyable text and keep authenticated decision-pack context out of public GitHub text.", - "Do not request wallets, hotkeys, coldkeys, private keys, GitHub tokens, or local source contents.", - ], - whenNotToUse: "Do not use this profile to chase compensation, predict public scores, or automate submissions without maintainer review.", - }, - "miner-auto-dev": { - id: "miner-auto-dev", - title: "Miner auto-dev", - audience: "miners running a local harness (Claude Code/Codex/Cursor) for reward-aware, gate-throttled OSS auto-development", - purpose: "Drive a plan→implement→push loop: pick reward-optimal work, plan it as a step DAG, let YOUR harness implement it locally, and push via local write-tools — always behind the LoopOver gate and the anti-slop throttle.", - recommendedPrompts: ["loopover_miner_select_issue", "loopover_miner_cleanup_first", "loopover_miner_draft_pr_packet"], - recommendedTools: [ - "loopover_agent_plan_next_work", - "loopover_run_local_scorer", - "loopover_build_plan", - "loopover_plan_status", - "loopover_record_step_result", - "loopover_preflight_current_branch", - "loopover_preview_local_pr_score", - "loopover_check_slop_risk", - "loopover_predict_gate", - "loopover_agent_prepare_pr_packet", - "loopover_create_branch", - "loopover_open_pr", - "loopover_file_issue", - "loopover_apply_labels", - "loopover_post_eligibility_comment", - "loopover_delete_branch", - ], - drivingLoop: [ - "Select: pull plan-next-work to pick the highest reward-optimal action. Respect your open-PR budget, credibility floor, and time-decay — skip work that would exceed your open-PR gate or chase low-credibility submissions.", - "Plan: build a step DAG (loopover_build_plan) for the chosen work and advance it with loopover_record_step_result as each step completes; loopover_plan_status gives the next ready steps and lets you resume.", - "Implement: for a code step, run loopover_create_branch, let YOUR harness write the change locally, then run your validation suite.", - "Gate-check: run loopover_run_local_scorer + loopover_check_slop_risk + loopover_preflight_current_branch (and loopover_predict_gate) to confirm the change is substantive, slop-free, and gate-ready. If it trips slop or fails preflight, fix it locally or skip the step — never push it.", - "Push: only once the gate is satisfied, call the local write-tools (open_pr / file_issue / apply_labels / post_eligibility_comment) and run the returned command with YOUR own credentials. LoopOver supplies the content and the gate; it never performs the write and never sees your source.", - ], - boundaries: [ - "Reward-aware throttle: respect the open-PR gate, your credibility floor, and time-decay — never push work that fails preflight, trips the anti-slop check, or exceeds your open-PR budget.", - "Local execution: every GitHub write is run by YOUR harness with YOUR credentials via a write-tool's returned command. LoopOver supplies content + gates only; it never performs the write and never receives your source contents.", - "Do not request wallets, hotkeys, coldkeys, private keys, GitHub tokens, or upload local source contents.", - ], - whenNotToUse: "Do not use this profile to bypass the gate, mass-open PRs, farm low-credibility submissions, or push changes that fail preflight or trip the anti-slop check.", - }, - "maintainer-triage": { - id: "maintainer-triage", - title: "Maintainer queue triage", - audience: "maintainers preparing low-noise queue and PR review context", - purpose: "Summarize queue risk, prepare review notes, and draft public guidance for human review.", - recommendedPrompts: ["loopover_maintainer_queue_triage", "loopover_maintainer_review_prep", "loopover_maintainer_public_guidance"], - recommendedTools: ["loopover_get_repo_context", "loopover_get_burden_forecast", "loopover_preflight_pr", "loopover_get_skipped_pr_audit"], - boundaries: [ - "Human-approved only: prepare summaries and draft guidance; do not post comments, label, close, merge, or edit contributor work.", - "Keep private review context, raw trust context, and authenticated-only evidence out of public snippets.", - "Do not request wallets, hotkeys, coldkeys, private keys, GitHub tokens, or local source contents.", - ], - whenNotToUse: "Do not use this profile as an autonomous maintainer bot or for public ranking, public scoring, or compensation claims.", - }, - "repo-owner-intake": { - id: "repo-owner-intake", - title: "Repo-owner intake", - audience: "repository owners preparing intake readiness and onboarding plans", - purpose: "Review registration readiness, focus manifests, docs/onboarding gaps, and manual setup actions.", - recommendedPrompts: ["loopover_repo_owner_intake_readiness", "loopover_repo_owner_focus_manifest_review", "loopover_repo_owner_onboarding_pack"], - recommendedTools: ["loopover_get_repo_context", "loopover_get_issue_quality", "loopover_get_registration_readiness", "loopover_get_config_recommendation"], - boundaries: [ - "Human-approved only: review, explain, and draft setup plans; do not push config, label issues, post comments, close issues, or publish public output.", - "Separate public readiness guidance from private maintainer or authenticated owner context.", - "Do not request wallets, hotkeys, coldkeys, private keys, GitHub tokens, or local source contents.", - ], - whenNotToUse: "Do not use this profile to bypass owner approval, auto-register repositories, or publish policy changes automatically.", - }, -}; -const configPath = process.env.LOOPOVER_CONFIG_PATH ?? - (process.env.LOOPOVER_CONFIG_DIR - ? join(process.env.LOOPOVER_CONFIG_DIR, "config.json") - : join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "loopover", "config.json")); -const cacheDir = process.env.LOOPOVER_CACHE_DIR ?? join(dirname(configPath), "cache"); -const decisionPackCacheDir = join(cacheDir, "decision-packs"); -const config = loadConfig(); -const requestedProfileName = cliOptionValue(cliArgs, "profile") ?? process.env.LOOPOVER_PROFILE; -const activeProfileName = selectProfileName(config, requestedProfileName); -const activeProfile = config.profiles?.[activeProfileName] ?? {}; -const configuredApiUrl = typeof activeProfile.apiUrl === "string" ? activeProfile.apiUrl.replace(/\/+$/, "") : typeof config.apiUrl === "string" ? config.apiUrl.replace(/\/+$/, "") : undefined; -const apiUrl = (process.env.LOOPOVER_API_URL ?? (configuredApiUrl && !legacyDefaultApiUrls.has(configuredApiUrl) ? configuredApiUrl : defaultApiUrl)).replace(/\/+$/, ""); -const ownerRepoShape = { - owner: z.string().min(1), - repo: z.string().min(1), -}; -const skippedPrAuditShape = { - repoFullName: z.string().trim().min(1).max(200).optional(), - reason: z.string().trim().min(1).max(64).optional(), - since: z.string().trim().min(1).max(64).optional(), - limit: z.number().int().positive().optional(), -}; -const ownerRepoPullShape = { - owner: z.string().min(1), - repo: z.string().min(1), - number: z.number().int().positive(), -}; -// #6736: the remote loopover_get_bounty_advisory tool's input shape (src/mcp/server.ts's bountyShape) -- -// a single cached-bounty id, GET /v1/bounties/:id/advisory. -const bountyAdvisoryShape = { - id: z.string().min(1), -}; -// #6619: same PR coordinates plus the OPTIONAL author login. Omitted, it resolves from the local session / -// LOOPOVER_LOGIN / GITHUB_LOGIN, so an already-logged-in contributor never has to retype their own login. -const prAiReviewFindingsShape = { - ...ownerRepoPullShape, - login: z.string().min(1).optional(), -}; -// #6149 write-tool input shapes -- mirror src/mcp/server.ts's remote shapes (same bounds) so the local -// server validates identically. The builders (buildOpenPrSpec, ...) are the same @loopover/engine functions. -const WRITE_TOOL_REPO_FULL_NAME_MAX = 200; -const WRITE_TOOL_BRANCH_REF_MAX = 200; -const WRITE_TOOL_TITLE_MAX = 400; -const WRITE_TOOL_BODY_MAX = 60000; -const WRITE_TOOL_BRANCH_MAX = 255; -// Mirrors @loopover/engine/signals/test-evidence's TEST_FRAMEWORKS (the detectTestConvention framework set), -// so a caller cannot request a test-gen spec for a framework the detector could never produce -- same guard the -// remote server's testGenShape uses. -const TEST_FRAMEWORKS = ["vitest", "jest", "pytest", "go-test", "rspec", "cargo-test"]; -const writeToolRepoFullName = z.string().min(3).max(WRITE_TOOL_REPO_FULL_NAME_MAX); -const openPrShape = { - repoFullName: writeToolRepoFullName, - base: z.string().min(1).max(WRITE_TOOL_BRANCH_REF_MAX), - head: z.string().min(1).max(WRITE_TOOL_BRANCH_REF_MAX), - title: z.string().min(1).max(WRITE_TOOL_TITLE_MAX), - body: z.string().max(WRITE_TOOL_BODY_MAX), - draft: z.boolean().optional(), -}; -const fileIssueShape = { - repoFullName: writeToolRepoFullName, - title: z.string().min(1).max(WRITE_TOOL_TITLE_MAX), - body: z.string().max(WRITE_TOOL_BODY_MAX), - labels: z.array(z.string().min(1).max(100)).max(20).optional(), -}; -const applyLabelsShape = { - repoFullName: writeToolRepoFullName, - number: z.number().int().positive(), - labels: z.array(z.string().min(1).max(100)).min(1).max(20), -}; -const closePrShape = { - repoFullName: writeToolRepoFullName, - number: z.number().int().positive(), - comment: z.string().max(WRITE_TOOL_BODY_MAX).optional(), -}; -const postEligibilityCommentShape = { - repoFullName: writeToolRepoFullName, - number: z.number().int().positive(), - body: z.string().min(1).max(WRITE_TOOL_BODY_MAX), -}; -const createBranchShape = { - branch: z.string().min(1).max(WRITE_TOOL_BRANCH_MAX), - base: z.string().min(1).max(WRITE_TOOL_BRANCH_MAX).optional(), -}; -const deleteBranchShape = { - branch: z.string().min(1).max(WRITE_TOOL_BRANCH_MAX), - remote: z.boolean().optional(), -}; -const testGenShape = { - repoFullName: writeToolRepoFullName, - targetFiles: z.array(z.string().min(1).max(500)).min(1).max(50), - framework: z.enum(TEST_FRAMEWORKS), - testDir: z.string().min(1).max(255).optional(), - criteria: z.array(z.string().min(1).max(300)).max(20).optional(), -}; -const followUpIssueShape = { - repoFullName: writeToolRepoFullName, - path: z.string().min(1).max(500), - line: z.number().int().positive().optional(), - finding: z.string().min(1).max(WRITE_TOOL_BODY_MAX), - label: z.string().min(1).max(100).optional(), -}; -const loginShape = { - login: z.string().min(1), -}; -const loginRepoShape = { - login: z.string().min(1), - owner: z.string().min(1), - repo: z.string().min(1), -}; -const validateLinkedIssueShape = { - owner: z.string().min(1), - repo: z.string().min(1), - issueNumber: z.number().int().positive(), - plannedChange: z - .object({ - title: z.string().min(1).optional(), - changedFiles: z.array(z.string()).optional(), - contributorLogin: z.string().min(1).optional(), - }) - .optional(), -}; -const checkBeforeStartShape = { - owner: z.string().min(1), - repo: z.string().min(1), - issueNumber: z.number().int().positive().optional(), - title: z.string().min(1).optional(), - plannedPaths: z.array(z.string()).optional(), -}; -const feasibilityGateShape = { - claimStatus: z.enum(["unclaimed", "claimed", "solved", "unknown"]), - duplicateClusterRisk: z.enum(["none", "low", "medium", "high"]), - issueStatus: z.enum(["ready", "needs_proof", "hold", "do_not_use", "duplicate", "invalid", "missing"]), - found: z.boolean().optional(), - // Optional: when both are supplied AND a local loopover-miner install's claim ledger is present (#5157), claimStatus is - // read from that ledger instead of trusting this caller-supplied value. Omitting either falls back to - // today's caller-supplied-string behavior unchanged. - repoFullName: z.string().min(1).optional(), - issueNumber: z.number().int().positive().optional(), -}; -/** - * Read-only lookup of the caller's own claim status from a local loopover-miner install's claim ledger - * (#5157), so `loopover_feasibility_gate` isn't purely trusting a caller-supplied `claimStatus` string. - * Returns `null` (fall back to the caller-supplied value unchanged) only when there is genuinely nothing to - * look up: no repo/issue supplied, no local install detected (the ledger DB file doesn't exist -- checked - * via `existsSync` BEFORE opening anything), or the sibling `@loopover/miner` package isn't - * resolvable at all (a standalone loopover-mcp install with no miner alongside it). When the ledger DB - * file DOES exist (a real local install IS present) but reading it fails -- corrupt, locked, permission - * denied -- this returns `"unknown"` rather than silently falling back to a caller-supplied string that - * ground-truth data (which we know exists but can't currently read) might contradict; `"unknown"` is an - * existing, honest claimStatus value the calculator already understands, not a guess. - * - * Uses `openClaimLedgerReadOnly` (not `openClaimLedger`), which opens the DB file in SQLite's own `readonly` - * mode -- a DRIVER-ENFORCED guarantee, not just a by-convention one. `openClaimLedger` always runs - * `CREATE TABLE IF NOT EXISTS` plus a schema-version stamp on open, which IS a write even against a file - * that merely exists but is empty/uninitialized; this tool never calls that, `recordClaim`, - * `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. - */ -async function resolveLedgerClaimStatus(repoFullName, issueNumber) { - if (!repoFullName || !issueNumber) - return null; - let claimLedgerModule; - try { - claimLedgerModule = await import("@loopover/miner/lib/claim-ledger.js"); - } - 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 - resolves */ - return null; - } - const { resolveClaimLedgerDbPath, openClaimLedgerReadOnly } = claimLedgerModule; - const dbPath = resolveClaimLedgerDbPath(); - if (!existsSync(dbPath)) - return null; - try { - const ledger = openClaimLedgerReadOnly(dbPath); - try { - const activeClaims = ledger.listActiveClaims(repoFullName); - return activeClaims.some((claim) => claim.issueNumber === issueNumber) ? "claimed" : "unclaimed"; - } - finally { - ledger.close(); - } - } - catch { - // The ledger DB file exists (a real local install IS present) but reading it failed -- corrupt, locked, - // a permission error, or not actually a claim-ledger database. Never silently trust a caller-supplied - // string that could contradict ground truth we know exists but can't currently read; "unknown" surfaces - // that honestly instead of guessing. - return "unknown"; - } -} -const findOpportunitiesShape = { - targets: z - .array(z.object({ - owner: z.string().min(1), - repo: z.string().min(1), - })) - .optional(), - searchQuery: z.string().min(1).max(500).optional(), - goalSpec: z - .object({ - lane: z.string().min(1).optional(), - minRankScore: z.number().min(0).max(100).optional(), - languages: z.array(z.string()).optional(), - }) - .optional(), - limit: z.number().int().min(1).max(50).optional(), -}; -const issueRagShape = { - owner: z.string(), - repo: z.string(), - title: z.string(), - body: z.string().optional(), - labels: z.array(z.string()).optional(), - topK: z.number().int().min(1).max(12).optional(), -}; -const lintPrTextShape = { - commitMessages: z.array(z.string()).max(50).optional(), - prBody: z.string().optional(), - linkedIssue: z.number().int().positive().optional(), -}; -const validateConfigShape = { - content: z.string().max(256 * 1024), - source: z.enum(["repo_file", "api_record", "none"]).optional(), -}; -// #6754: mirrors evaluateEscalationShape in src/mcp/server.ts exactly, so the local tool, the remote tool, and -// the REST route all accept an identical payload. -const evaluateEscalationShape = { - runStatus: z.enum(["running", "converged", "abandoned", "error"]), - healthStatus: z.enum(["healthy", "degraded", "critical"]).optional(), - customerFlagged: z.boolean().optional(), - killRequested: z.boolean().optional(), -}; -// #6755: mirrors intakeIdeaShape in src/mcp/server.ts exactly, so the local tool, the remote tool, and the REST -// route all accept an identical payload. Deliberately loose -- validateIdeaSubmission owns the real checks. -const intakeIdeaShape = { - id: z.string().optional(), - title: z.string().optional(), - body: z.string().optional(), - targetRepo: z.string().optional(), - constraints: z.array(z.string()).max(50).optional(), - acceptanceHints: z.array(z.string()).max(50).optional(), - priority: z.string().optional(), - decomposition: z - .array(z.object({ key: z.string(), title: z.string(), body: z.string(), dependsOn: z.array(z.string()).max(50).optional() })) - .max(50) - .optional(), -}; -// #6752: mirrors buildResultsPayloadShape in src/mcp/server.ts exactly, so the local tool, the remote tool, and -// the REST route all accept an identical payload. -const resultsPayloadShape = { - repoFullName: z.string().min(1), - prNumber: z.number().int().nullable().optional(), - title: z.string(), - changedFiles: z - .array(z.object({ path: z.string(), additions: z.number().int().optional(), deletions: z.number().int().optional() })) - .max(5000) - .optional(), - status: z.enum(["open", "merged", "closed"]).optional(), -}; -// #6753: mirrors buildProgressSnapshotShape in src/mcp/server.ts exactly, so the local tool, the remote tool, and -// the REST route all accept an identical payload. -const buildProgressSnapshotShape = { - iteration: z.number().int(), - maxIterations: z.number().int().nullable().optional(), - phase: z.enum(["queued", "claiming", "coding", "reviewing", "submitting", "done"]), - status: z.enum(["running", "converged", "abandoned", "error"]), - recentActivity: z - .array(z.object({ step: z.string(), detail: z.string().optional(), at: z.string().optional() })) - .max(1000) - .optional(), -}; -// #6749: mirrors checkTestEvidenceShape in src/mcp/server.ts VERBATIM (same bounds, same optionality). -const checkTestEvidenceShape = { - changedPaths: z.array(z.string().min(1).max(400)).max(2000), - testFiles: z.array(z.string().min(1).max(400)).max(2000).optional(), - tests: z.array(z.string().max(400)).max(2000).optional(), -}; -// #6750: mirrors suggestBoundaryTestsShape in src/mcp/server.ts VERBATIM. -const suggestBoundaryTestsShape = { - changedFiles: z.array(z.object({ path: z.string().min(1).max(400) }).strict()).max(500), - boundaryTouches: z - .array(z.object({ path: z.string().min(1).max(400), kind: z.enum(["array_index_bounds", "null_or_undefined_branch", "empty_collection_check"]) }).strict()) - .max(20) - .optional(), - tests: z.array(z.string().max(400)).max(2000).optional(), - testFiles: z.array(z.string().max(400)).max(2000).optional(), -}; -// #6751: mirrors simulateOpenPrPressureShape in src/mcp/server.ts VERBATIM. The bin cannot import from src/ -// (package boundary), so this copy is the one place parity is by convention rather than construction — the -// route parses with the tool's own exported shape, and mcp-cli-open-pr-pressure-tool.test.ts pins that a -// payload this shape accepts is one the route accepts too. -const simulateOpenPrPressureCount = z.number().int().min(0).max(1000000); -const simulateOpenPrPressureShape = { - repoFullName: z.string().min(3).max(200), - generatedAt: z.string().min(1).max(100), - queueHealth: z - .object({ - repoFullName: z.string().min(3).max(200), - generatedAt: z.string().min(1).max(100), - burdenScore: z.number().finite(), - level: z.enum(["low", "medium", "high", "critical"]), - summary: z.string().max(1000), - signals: z - .object({ - openIssues: simulateOpenPrPressureCount, - openPullRequests: simulateOpenPrPressureCount, - unlinkedPullRequests: simulateOpenPrPressureCount, - stalePullRequests: simulateOpenPrPressureCount, - draftPullRequests: simulateOpenPrPressureCount, - maintainerAuthoredPullRequests: simulateOpenPrPressureCount, - collisionClusters: simulateOpenPrPressureCount, - ageBuckets: z - .object({ under7Days: simulateOpenPrPressureCount, days7To30: simulateOpenPrPressureCount, over30Days: simulateOpenPrPressureCount }) - .passthrough(), - likelyReviewablePullRequests: simulateOpenPrPressureCount, - cachedOpenPullRequests: simulateOpenPrPressureCount.optional(), - likelyReviewablePullRequestsSource: z.enum(["cache", "sampled_cache", "authoritative"]).optional(), - }) - .passthrough(), - findings: z.array(z.unknown()).max(100), - }) - .passthrough() - .nullable(), - roleContext: z.object({ maintainerLane: z.boolean() }).passthrough(), - contributorOpenPrCount: simulateOpenPrPressureCount.optional(), -}; -const checkSlopRiskShape = { - changedFiles: z - .array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() })) - .max(2000) - .optional(), - description: z.string().max(20000).optional(), - tests: z.array(z.string().max(400)).max(2000).optional(), - testFiles: z.array(z.string().max(400)).max(2000).optional(), -}; -const checkIssueSlopShape = { - title: z.string().max(500).optional(), - body: z.string().max(40000).optional(), -}; -// #6150 — loopover_run_local_scorer's input, mirroring the remote server's changedFileSchema/validationEntrySchema. -const localScorerChangedFileShape = z - .object({ - path: z.string().min(1).max(400), - previousPath: z.string().min(1).max(400).optional(), - additions: z.number().int().min(0).optional(), - deletions: z.number().int().min(0).optional(), - status: z.enum(["added", "modified", "deleted", "renamed", "copied", "unknown"]).optional(), - binary: z.boolean().optional(), -}) - .strict(); -const localScorerValidationShape = z - .object({ - command: z.string().min(1).max(400), - status: z.enum(["passed", "failed", "not_run", "skipped", "focused", "unknown"]), - summary: z.string().max(2000).optional(), - durationMs: z.number().int().min(0).optional(), - exitCode: z.number().int().min(0).optional(), -}) - .strict(); -const runLocalScorerShape = { - changedFiles: z.array(localScorerChangedFileShape).min(1).max(500), - validation: z.array(localScorerValidationShape).max(50).optional(), -}; -// #6150 — loopover_build_plan/loopover_plan_status/loopover_record_step_result's input, mirroring the remote -// server's rawPlanStepSchema/planStepSchema/planDagSchema (src/mcp/server.ts). -const rawPlanStepShape = z - .object({ - id: z.string().min(1).max(100), - title: z.string().min(1).max(300), - actionClass: z.string().min(1).max(60).optional(), - dependsOn: z.array(z.string().min(1).max(100)).max(50).optional(), - maxAttempts: z.number().int().min(1).max(10).optional(), -}) - .strict(); -const planStepShape = z - .object({ - id: z.string().min(1).max(100), - title: z.string().min(1).max(300), - actionClass: z.string().min(1).max(60).optional(), - dependsOn: z.array(z.string().min(1).max(100)).max(50), - status: z.enum(["pending", "running", "completed", "failed", "skipped"]), - attempts: z.number().int().min(0), - maxAttempts: z.number().int().min(1).max(10), - lastError: z.string().max(2000).nullable().optional(), -}) - .strict(); -const planDagShape = z.object({ steps: z.array(planStepShape).max(100) }).strict(); -const buildPlanShape = { steps: z.array(rawPlanStepShape).min(1).max(100) }; -const planStatusShape = { plan: planDagShape }; -const recordStepResultShape = { - plan: planDagShape, - stepId: z.string().min(1).max(100), - outcome: z.enum(["completed", "failed", "skipped"]), - error: z.string().max(2000).optional(), -}; -// #6150 — loopover_predict_gate's input, mirroring the remote server's predictGateShape. Metadata-only (no -// git/workspace context needed): predicts the gate outcome for a PLANNED PR before any local code exists, the -// same use case loopover_preflight_pr already serves for lane/duplicate/linked-issue checks. -const predictGateShape = { - login: z.string().min(1), - owner: z.string().min(1), - repo: z.string().min(1), - title: z.string().min(1), - body: z.string().max(40000).optional(), - labels: z.array(z.string()).max(50).optional(), - linkedIssues: z.array(z.number().int().positive()).max(50).optional(), - changedPaths: z.array(z.string().min(1).max(400)).max(500).optional(), -}; -const preflightShape = { - repoFullName: z.string().min(3), - contributorLogin: z.string().min(1).optional(), - title: z.string().min(1), - body: z.string().optional(), - labels: z.array(z.string()).optional(), - changedFiles: z.array(z.string()).optional(), - linkedIssues: z.array(z.number().int().positive()).optional(), - tests: z.array(z.string()).optional(), - authorAssociation: z.string().optional(), -}; -const localDiffShape = { - repoFullName: z.string().min(3), - cwd: z.string().optional(), - baseRef: z.string().default("HEAD"), - contributorLogin: z.string().min(1).optional(), - title: z.string().optional(), - body: z.string().optional(), - labels: z.array(z.string()).optional(), - linkedIssues: z.array(z.number().int().positive()).optional(), - tests: z.array(z.string()).optional(), - authorAssociation: z.string().optional(), - commitMessage: z.string().optional(), -}; -const branchEligibilityShape = { - status: z.enum(["eligible", "ineligible", "unknown"]), - source: z.enum(["github_metadata", "local_metadata", "registry", "user_supplied"]).optional(), - reason: z.string().optional(), - checkedAt: z.string().optional(), - stale: z.boolean().optional(), -}; -const localScoreShape = { - ...localDiffShape, - targetKey: z.string().optional(), - sourceTokenScore: z.number().min(0).optional(), - totalTokenScore: z.number().min(0).optional(), - sourceLines: z.number().min(0).optional(), - linkedIssueMode: z.enum(["none", "standard", "maintainer"]).default("none"), - openPrCount: z.number().int().min(0).optional(), - credibility: z.number().min(0).max(1).optional(), - changesRequestedCount: z.number().int().min(0).optional(), - pendingMergedPrCount: z.number().int().min(0).optional(), - pendingClosedPrCount: z.number().int().min(0).optional(), - approvedPrCount: z.number().int().min(0).optional(), - expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(), - projectedCredibility: z.number().min(0).max(1).optional(), - scenarioNotes: z.array(z.string()).optional(), - branchEligibility: z.object(branchEligibilityShape).strict().optional(), - scorePreviewCommand: z.string().optional(), -}; -const variantsShape = { - variants: z.array(z.object(localScoreShape)).min(1).max(10), -}; -const currentBranchShape = { - login: z.string().min(1), - cwd: z.string().optional(), - repoFullName: z.string().min(3).optional(), - baseRef: z.string().optional(), - headRef: z.string().optional(), - branchName: z.string().optional(), - title: z.string().optional(), - body: z.string().optional(), - labels: z.array(z.string()).optional(), - linkedIssues: z.array(z.number().int().positive()).optional(), - pendingMergedPrCount: z.number().int().min(0).optional(), - pendingClosedPrCount: z.number().int().min(0).optional(), - approvedPrCount: z.number().int().min(0).optional(), - expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(), - projectedCredibility: z.number().min(0).max(1).optional(), - scenarioNotes: z.array(z.string()).optional(), - branchEligibility: z.object(branchEligibilityShape).strict().optional(), - validation: z - .array(z.object({ - command: z.string().min(1), - status: z.enum(["passed", "failed", "not_run", "skipped", "focused", "unknown"]), - summary: z.string().optional(), - durationMs: z.number().int().min(0).optional(), - exitCode: z.number().int().min(0).optional(), - })) - .optional(), - scorePreviewCommand: z.string().optional(), -}; -const currentBranchVariantsShape = { - variants: z.array(z.object(currentBranchShape)).min(1).max(10), -}; -const agentPlanShape = { - login: z.string().min(1), - objective: z.string().optional(), - repoFullName: z.string().min(3).optional(), -}; -const agentRunShape = { - objective: z.string().min(1), - actorLogin: z.string().min(1), - targetRepoFullName: z.string().min(3).optional(), - targetPullNumber: z.number().int().positive().optional(), - targetIssueNumber: z.number().int().positive().optional(), -}; -const agentRunIdShape = { - runId: z.string().min(1), -}; -// #6152 maintain-surface tools. Each shape mirrors its already-shipped remote counterpart in src/mcp/server.ts -// (listPendingActionsShape, decidePendingActionShape, setAgentPausedShape, setActionAutonomyShape, -// ownerRepoWindowShape) so the same call works against either server. The `decision` verb is accept|reject -- -// the approval-queue route's own vocabulary (#779) -- rather than the maintain CLI's approve|reject, because a -// tool caller is talking to the route, not to the CLI's surface. -// -// One deliberate divergence: the remote's listPendingActionsShape takes an optional `status`, which it can honour -// because it queries the approval-queue store directly. This server reaches the queue only through -// GET /v1/repos/:owner/:repo/agent/pending-actions, which takes no query parameters and hardcodes status -// "pending" (src/api/routes.ts). Offering a `status` here would let a caller ask for "rejected", get the pending -// list, and be told it succeeded -- so it is left out of the schema and the description names the queue as the -// pending one. An agent picks its arguments from the published schema, so a filter that isn't there is one it -// won't ask for; a key sent anyway is dropped by the MCP layer before this handler and never reaches the URL. -const listPendingActionsShape = { - owner: z.string().min(1), - repo: z.string().min(1), -}; -const decidePendingActionShape = { - owner: z.string().min(1), - repo: z.string().min(1), - id: z.string().min(1), - decision: z.enum(["accept", "reject"]), -}; -const setAgentPausedShape = { - owner: z.string().min(1), - repo: z.string().min(1), - paused: z.boolean(), -}; -// Reuses the CLI's own constants, so `maintain set-level`'s validation and this tool's schema can never disagree -// about what the server accepts. -const setActionAutonomyShape = { - owner: z.string().min(1), - repo: z.string().min(1), - action: z.enum(MAINTAIN_ACTION_CLASSES), - level: z.enum(MAINTAIN_AUTONOMY_LEVELS), -}; -const gatePrecisionShape = { - owner: z.string().min(1), - repo: z.string().min(1), - windowDays: z.number().int().positive().optional(), -}; -// Single source of truth for stdio tool name + one-line description (#2233). -// Registration and `loopover-mcp tools` both read this list. -const STDIO_TOOL_DESCRIPTORS = [ - { - name: "loopover_get_repo_context", - category: "maintainer", - description: "Return the LoopOver repo-context bundle for a repo — registration state, recommended contribution lane, queue health, duplicate-PR collisions, and config quality — from the private LoopOver API. Takes owner and repo.", - }, - { - name: "loopover_get_pr_reviewability", - category: "review", - description: "Return the reviewability report for an open PR: how ready it is to review/merge, the blocking or advisory signals against it, and its lane/duplicate/linked-issue context. Metadata-only, no GitHub writes.", - }, - { - name: "loopover_get_pr_ai_review_findings", - category: "review", - description: "Return a submitted pull request's real AI-review inline findings as structured JSON (category, path, severity, line, body) — the same categorization the PR comment uses. Post-submission only; self-scoped to your own PRs. Metadata-only, no GitHub writes.", - }, - { - name: "loopover_get_maintainer_noise", - category: "maintainer", - description: "Return the maintainer queue-noise triage report for a repo: a noise score/level, the specific noise sources to clear first, and recommended maintainer actions. Maintainer-authenticated; advisory only.", - }, - { - name: "loopover_preflight_pr", - category: "discovery", - description: "Preflight planned PR metadata against lane, duplicate, linked issue, test, and queue signals.", - }, - { - name: "loopover_explain_review_risk", - category: "review", - description: "Explain review risk for a planned PR using preflight, lane, duplicate, and role context.", - }, - { - name: "loopover_validate_linked_issue", - category: "discovery", - description: "Report whether linking an issue will actually earn the standard linked-issue scoring multiplier for a planned PR — open, valid, single-owner, solvable by this PR — with the blocking reason if not. The raw multiplier value stays private.", - }, - { - name: "loopover_check_before_start", - category: "discovery", - description: "Before writing any code, check whether an issue is already claimed or solved, whether a duplicate cluster is forming, and whether it is a valid target. Returns a go/raise/avoid recommendation with public-safe reasons from cached metadata.", - }, - { - name: "loopover_find_opportunities", - category: "discovery", - description: "Cross-repo discovery: find high-fit contribution opportunities across registered Gittensor repos. Returns a ranked, public-safe list filtered by your MinerGoalSpec (lane, min rank score, languages). Metadata-only, no GitHub writes.", - }, - { - name: "loopover_retrieve_issue_context", - category: "discovery", - description: "Repo-scoped issue-centric RAG retrieval for the miner analyze phase. Returns related file paths and retrieval scores from issue title/body/labels — metadata only, never source text.", - }, - { - name: "loopover_lint_pr_text", - category: "review", - description: "Lint a commit message + PR body against the gittensor traceability/no-issue-rationale and Conventional Commit rubric before submitting. Returns a deterministic verdict (strong/adequate/weak) plus specific public-safe fixes. Computed in-process; no source upload and no API round-trip.", - }, - { - name: "loopover_validate_config", - category: "utility", - description: "Parse and validate a .loopover.yml manifest string using the same focus-manifest parser as the server. Returns normalized config fields, parse warnings, and an ok/warn/error status. Computed in-process; no source upload and no API round-trip. Metadata-only, no GitHub writes.", - }, - { - name: "loopover_check_slop_risk", - category: "review", - description: "Assess the deterministic slop risk of a planned change from local diff metadata (paths + line counts) + the PR description — an agent-native, source-free quality self-check. Returns slopRisk (0-100), band, findings, and the rubric. Computed in-process; no repo data and no API round-trip.", - }, - { - name: "loopover_simulate_open_pr_pressure", - category: "discovery", - description: "Rank what-if scenarios for easing a repo's open-PR pressure from already-computed queue-health metadata — deterministic, public-safe, and read-only. Needs no repo access and performs no GitHub writes.", - }, - { - name: "loopover_suggest_boundary_tests", - category: "review", - description: "Boundary-safe test-generation suggestion: evaluate locally precomputed boundary-touch metadata (path + pattern kind only; no patch/source text) with no test evidence in the diff, and return a LOCAL-execution action spec (criteria/hints only — never generated test code) for your OWN agent to scaffold tests with. Advisory-only; never blocks, never writes.", - }, - { - name: "loopover_check_test_evidence", - category: "review", - description: "Classify whether a planned change's changed files carry enough test evidence, from path metadata alone (no source uploaded) — an agent-native coverage-gap self-check before opening a PR. Returns a coverage band (strong/adequate/weak/absent) plus actionable guidance. Computed in-process; no API round-trip.", - }, - { - name: "loopover_evaluate_escalation", - category: "agent", - description: "Decide whether a rented loop needs a human, and what action to take, from an already-computed run outcome, health tier, and operator/customer signals — the deterministic support/escalation-path logic. Source-free; returns shouldEscalate + action (none/notify/human_review/stop) + severity + reasons. It decides; the caller wires the action. Computed in-process; no API round-trip.", - }, - { - name: "loopover_build_results_payload", - category: "agent", - description: "Package a completed loop iteration into the customer-facing result (#4801): a PR link, a plain-language summary, and a bounded diff preview, from already-computed iteration metadata. Deterministic and source-free — it formats the result, it does not fetch, open, or deliver anything. Computed in-process; no API round-trip.", - }, - { - name: "loopover_build_progress_snapshot", - category: "agent", - description: "Build a near-real-time progress snapshot for a running rented loop (#4800): phase, status, iteration/percent-complete, and a bounded recent-activity tail, from already-computed loop state. Deterministic and source-free; a customer surface pushes it on change rather than polling on a fixed interval. Computed in-process; no API round-trip.", - }, - { - name: "loopover_intake_idea", - category: "agent", - description: "Turn a freeform renter idea into a strict, claimable task-graph (spec #4779) and score it against the same feasibility gate the loop runs on. Deterministic and source-free: validates the submission, assembles constituent issues (an optional caller-supplied decomposition, else a single-issue baseline), and returns the graph plus its go/raise/avoid verdict. A malformed or empty submission returns an actionable error list, not a silent failure. Computed in-process; no API round-trip.", - }, - { - name: "loopover_plan_idea_claims", - category: "agent", - description: "Route a freeform idea through the intake bridge into a claim/code/submit-loop plan (#4799): validates the submission, builds the scored task-graph, and returns which constituent issues the loop can claim now vs. defer vs. skip — dependency-ordered so a prerequisite is always claimed before its dependents. Deterministic and source-free; it decides what to claim, it does not claim or run anything. Computed in-process; no API round-trip.", - }, - { - name: "loopover_check_issue_slop", - category: "review", - description: "Assess the deterministic slop risk of an issue from its title + body alone (no repo data) — flags clearly low-effort issues (empty body, an unfilled template) for triage. Returns slopRisk (0-100), band, findings, and the rubric. Advisory-only.", - }, - // #6150 — the miner-auto-dev profile's plan-DAG + local-scorer + gate-prediction tools, previously listed in - // recommendedTools below but never actually registered. - { - name: "loopover_run_local_scorer", - category: "branch", - description: "Compute deterministic source/test/non-code token scores from local changed-file metadata + validation results — no repo/contributor access, reveals nothing beyond a computation on the caller's own diff stats. Pass the result as the localScorer field of loopover_preview_local_pr_score or the analyze tools to score this branch in external_command mode. Computed in-process; no API round-trip.", - }, - { - name: "loopover_build_plan", - category: "agent", - description: "Build a normalized step DAG (dependencies, retry limits) from a raw list of steps and validate it for cycles/unknown dependencies. Returns the plan, its progress, the currently-ready steps, and validation. Computed in-process; no API round-trip.", - }, - { - name: "loopover_plan_status", - category: "agent", - description: "Return a plan's current progress, the next ready steps, and validation status. Takes the plan object returned by loopover_build_plan or a prior loopover_record_step_result call. Computed in-process; no API round-trip.", - }, - { - name: "loopover_record_step_result", - category: "agent", - description: "Record the outcome (completed/failed/skipped) of a plan step the harness just ran and return the updated plan. A failed step retries (back to pending) until its maxAttempts is exhausted. Computed in-process; no API round-trip.", - }, - { - name: "loopover_predict_gate", - category: "review", - description: "Predict the LoopOver gate outcome for a planned PR before any local code exists — the same advisory + gate evaluation the maintainer pipeline runs, using only the repo's public .loopover.yml policy. Takes login, owner, repo, title, and optional body/labels/linkedIssues/changedPaths. Metadata-only, no source upload.", - }, - { - name: "loopover_explain_gate_disposition", - category: "review", - description: "Explain WHY the LoopOver gate would pass or block a planned PR: the itemized per-rule dispositions (which specific gate rules block vs advise, and why) behind loopover_predict_gate's verdict. Read-only reasoning surface from the repo's PUBLIC .loopover.yml only — no merge/close decision. Self-scoped to the authenticated login.", - }, - { - name: "loopover_preflight_local_diff", - category: "branch", - description: "Inspect local git diff metadata and run LoopOver preflight without uploading source contents.", - }, - { - name: "loopover_get_registry_changes", - category: "utility", - description: "Return the latest cached report of changes to the Gittensor repo registry — repositories added, removed, or re-registered upstream. Read-only; takes no parameters.", - }, - { - name: "loopover_get_upstream_drift", - category: "utility", - description: "Return the latest cached Gittensor upstream ruleset drift status (stale/drift warnings) for MCP planning.", - }, - { - name: "loopover_get_bounty_advisory", - category: "discovery", - description: "Return the lifecycle, funding, and consensus-risk context for a cached Gittensor bounty by id, from the public LoopOver API.", - }, - { - name: "loopover_get_label_audit", - category: "maintainer", - description: "Return the repo's label-policy audit (configured-vs-live labels, missing configured labels, suspicious status/source-style labels, and trusted-label-pipeline readiness) from the private LoopOver API.", - }, - { - name: "loopover_get_maintainer_lane", - category: "maintainer", - description: "Return the repo's maintainer-lane triage report (the lane recommendation alongside the configured maintainer cut, queue health, config quality, and contributor-intake health) from the private LoopOver API. Advisory only.", - }, - { - name: "loopover_get_burden_forecast", - category: "maintainer", - description: "Return the repo's cached maintainer burden forecast (projected review load, queue-growth risk, and stale-PR signals) with a freshness marker, from the private LoopOver API.", - }, - { - name: "loopover_get_repo_outcome_patterns", - category: "maintainer", - description: "Return cached or freshly-computed per-repo accepted/rejected PR outcome patterns: what maintainers actually merge or close, separated from maintainer-lane activity, with a freshness marker and explicit evidence-completeness.", - }, - { - name: "loopover_preview_local_pr_score", - category: "branch", - description: "Inspect local diff metadata and request a private LoopOver scoring preview. No source contents are uploaded.", - }, - { - name: "loopover_explain_score_breakdown", - category: "review", - description: "Explain a private score preview multiplier-by-multiplier with plain-English levers and the highest-impact improvement.", - }, - { - name: "loopover_get_eligibility_plan", - category: "discovery", - description: "Derive a structured eligibility plan from local score-preview metadata: whether the branch/PR is eligible now, public-safe blockers, and cleanup paths. Advisory dry-run only — no GitHub writes.", - }, - { - name: "loopover_get_decision_pack", - category: "discovery", - description: "Return the private decision pack for a contributor: the ranked repos and issues to work on next, with per-repo go/raise/avoid guidance. Takes login (the contributor's GitHub username).", - }, - { - name: "loopover_explain_repo_decision", - category: "discovery", - description: "Return the go/raise/avoid decision for one specific contributor-and-repo pair, drawn from that contributor's decision pack — narrower than loopover_get_decision_pack, which returns the whole pack. Takes login (GitHub username), owner, and repo.", - }, - { - name: "loopover_monitor_open_prs", - category: "discovery", - description: "Inspect a contributor's open PRs on registered repos, classify queue state, and return public-safe next-step packets from cached metadata.", - }, - { - name: "loopover_pr_outcome", - category: "review", - description: "Return a contributor's own post-merge outcome records — for each merged PR, a public-safe attribution of what it did for their standing on the repo. Self-scoped: only the authenticated login's outcomes.", - }, - { - name: "loopover_compare_pr_variants", - category: "branch", - description: "Compare private LoopOver scoring previews across local/metadata variants.", - }, - { - name: "loopover_local_status", - category: "utility", - description: "Return local LoopOver MCP status, inferred git repo metadata, and privacy defaults.", - }, - { - name: "loopover_preflight_current_branch", - category: "branch", - description: "Analyze the current git branch and return PR readiness. Sends metadata only.", - }, - { - name: "loopover_review_pr_before_push", - category: "branch", - description: "Run a single composed pre-PR review of the current branch: preflight (lane/duplicate/linked-issue/test/queue fit), slop-risk, and PR-text lint, merged into one report with an overall pass/warn/fail status. Thin composition of the existing checks — does not reimplement any of them. Sends metadata only, no source upload.", - }, - { - name: "loopover_preview_current_branch_score", - category: "branch", - description: "Analyze the current git branch and return private scoreability context. Sends metadata only.", - }, - { - name: "loopover_rank_local_next_actions", - category: "branch", - description: "Analyze the current git branch and rank local next actions by private reward/risk and review friction.", - }, - { - name: "loopover_explain_local_blockers", - category: "branch", - description: "Analyze the current git branch and explain private scoreability, lane, and review blockers.", - }, - { - name: "loopover_remediation_plan", - category: "branch", - description: "Analyze the current git branch and return an ordered public-safe remediation checklist with rerun conditions.", - }, - { - name: "loopover_prepare_pr_packet", - category: "branch", - description: "Analyze the current git branch and return a public-safe PR packet. Sends metadata only.", - }, - { - name: "loopover_draft_pr_body", - category: "branch", - description: "Draft a public-safe, copy/paste PR body from local branch metadata (changed files, tests run, linked issue, duplicate/WIP caution, branch freshness, next steps). Private scoreability/reward/trust context is excluded; source contents are not uploaded. Optional format=markdown returns the rendered body as the primary payload.", - }, - { - name: "loopover_compare_local_variants", - category: "branch", - description: "Compare current-branch metadata variants without uploading source contents.", - }, - { - name: "loopover_agent_plan_next_work", - category: "agent", - description: "Run the deterministic LoopOver planner for a contributor and return the single recommended next unit of work (repo, issue, and action). Planning only — does not queue or start a run. Takes login (GitHub username); optional objective and repoFullName narrow the result.", - }, - { - name: "loopover_agent_start_run", - category: "agent", - description: "Queue a new LoopOver automated-agent run for a contributor. Copilot mode only: it proposes and records work but takes no GitHub actions on its own. Takes objective (what to accomplish) and actorLogin (the contributor's GitHub username); returns the new run's id and status.", - }, - { - name: "loopover_agent_get_run", - category: "agent", - description: "Fetch a previously queued LoopOver agent run by its id, including current status and planned actions. Takes runId (the id returned by loopover_agent_start_run).", - }, - { - name: "loopover_agent_explain_next_action", - category: "agent", - description: "Explain the next deterministic action and blocker context for a GitHub login.", - }, - { - name: "loopover_agent_prepare_pr_packet", - category: "branch", - description: "Prepare a public-safe PR packet from current branch metadata. Sends metadata only.", - }, - { - name: "loopover_local_status_structured", - category: "utility", - description: "Return local LoopOver MCP status with a validated structured output schema.", - }, - { - name: "loopover_feasibility_gate", - category: "discovery", - description: "Pure local go/raise/avoid feasibility verdict from claim status, duplicate-cluster risk, and issue quality/lifecycle status — the same discriminants the analyze-phase feasibility gate branches on. When repoFullName/issueNumber are supplied and a local loopover-miner install's claim ledger is present, claimStatus is read from that ledger instead of the caller-supplied value; otherwise falls back to the caller-supplied claimStatus unchanged. Advisory-only — never blocks, cancels, or overrides a claim or attempt; real claim-conflict resolution authority stays with the maintainer-only path. No API round-trip.", - }, - { - name: "loopover_get_issue_quality", - category: "maintainer", - description: "Return the cached or freshly-computed issue-quality report for a repo, ranking which open issues are actionable, need proof, are stale/duplicate-prone, or already solved.", - }, - { - name: "loopover_get_registration_readiness", - category: "maintainer", - description: "Preview-only registration-readiness report for a repository: what's missing/present before/after registering with LoopOver (direct-PR and issue-discovery lane readiness, label policy, maintainer-cut readiness, queue health, docs, and the GitHub App install state). Advisory only, not a registration action.", - }, - { - name: "loopover_get_config_recommendation", - category: "maintainer", - description: "Return recommended .loopover.yml additions for a repository, derived from the repo's live, currently-active configured behavior (the raw dashboard/API-configured settings, not a yml-merged view — so the recommendation never compares itself against an override that already exists). Advisory only, not a write action.", - }, - { - name: "loopover_get_skipped_pr_audit", - category: "maintainer", - description: "Return the skipped-PR audit trail: pull requests LoopOver's automated reviewer intentionally stayed quiet on, each with a reason code and a remediation hint. Optionally filter by repoFullName, reason, or since. Maintainer-authenticated; read-only measurement, not a moderation or override action.", - }, - // #6152 — the maintain CLI's REST surface, exposed as tools so an agent can drive it without shelling out. - // Categories mirror the remote server's MCP_TOOL_CATEGORIES entries for the same names, so a caller sees one - // consistent grouping across both surfaces. - { - name: "loopover_list_pending_actions", - category: "agent", - description: "List the agent actions currently staged and awaiting a decision in a repo's approval queue, so a maintainer can review what is pending. Returns the pending queue only — the same list as `loopover-mcp maintain queue`. Maintainer access required.", - }, - { - name: "loopover_decide_pending_action", - category: "agent", - description: "Accept (execute) or reject a staged approval-queue action by id. Accept runs it through the live executor gates; reject cancels it. Scoped to this repo, same as `loopover-mcp maintain approve|reject `. Maintainer access required.", - }, - { - name: "loopover_set_agent_paused", - category: "agent", - description: "Pause or resume ALL agent actions on a repo (the kill-switch toggle), same as `loopover-mcp maintain pause|resume`. Maintainer access required.", - }, - { - name: "loopover_set_action_autonomy", - category: "agent", - description: "Set the autonomy level for one action class via a read-merge-write, so the other classes are left untouched. Same as `loopover-mcp maintain set-level `. Maintainer access required.", - }, - { - name: "loopover_get_gate_precision", - category: "maintainer", - description: "Return per-gate-type false-positive precision for a repo's recorded gate blocks — blocked / blocked-then-merged counts and false-positive rates with low-sample guards. Optionally bounded by windowDays. Maintainer-authenticated; measurement only.", - }, - { - name: "loopover_open_pr", - category: "agent", - description: "Build a LOCAL-execution spec to open a pull request from your branch (run it with your own gh creds; loopover never performs the write).", - }, - { - name: "loopover_file_issue", - category: "agent", - description: "Build a LOCAL-execution spec to file an issue (run it with your own gh creds; loopover never performs the write).", - }, - { - name: "loopover_apply_labels", - category: "agent", - description: "Build a LOCAL-execution spec to add labels to an issue or PR (run it with your own gh creds; loopover never performs the write).", - }, - { - name: "loopover_post_eligibility_comment", - category: "agent", - description: "Build a LOCAL-execution spec to post an eligibility/context comment on an issue or PR (run it with your own gh creds; loopover never performs the write).", - }, - { - name: "loopover_create_branch", - category: "agent", - description: "Build a LOCAL-execution spec to create a branch (run it locally; loopover never performs the write).", - }, - { - name: "loopover_delete_branch", - category: "agent", - description: "Build a LOCAL-execution spec to delete a branch (run it locally; loopover never performs the write).", - }, - { - name: "loopover_generate_tests", - category: "agent", - description: "Build a LOCAL-execution spec describing WHAT boundary-safe test cases should exist for the given target files, using the repo's detected framework/convention. LoopOver supplies the criteria; your OWN agent scaffolds and runs the actual test files locally -- no source code is uploaded and loopover never performs the write.", - }, - { - name: "loopover_file_follow_up_issue", - category: "agent", - description: "Build a LOCAL-execution spec to file a follow-up issue for a review finding a maintainer wants TRACKED rather than blocked on this PR. Composes a bounded, public-safe title/body from the finding (run it with your own gh creds; loopover never performs the write).", - }, - { - name: "loopover_close_pr", - category: "agent", - description: "Build a LOCAL-execution spec to close a pull request, optionally with a comment (run it with your own gh creds; loopover never performs the write).", - }, -]; -// #6301 — coarse tool categories for grouping `loopover-mcp tools` output. Ordered -// contributor-facing surfaces first, operator ones last; the `label` is the human-readable header. -// Every STDIO_TOOL_DESCRIPTORS entry carries a `category` id drawn from this list (asserted in tests). -const STDIO_TOOL_CATEGORIES = [ - { id: "discovery", label: "Discovery & planning" }, - { id: "branch", label: "Local branch & PR prep" }, - { id: "review", label: "Review & gate prediction" }, - { id: "agent", label: "Agent automation" }, - { id: "maintainer", label: "Maintainer & repo owner" }, - { id: "utility", label: "Registry, config & status" }, -]; -function stdioToolDescription(name) { - const tool = STDIO_TOOL_DESCRIPTORS.find((entry) => entry.name === name); - if (!tool) - throw new Error(`Unknown stdio tool descriptor: ${name}`); - return tool.description; -} -if (cliArgs[0] && cliArgs[0] !== "--stdio") { - try { - const exitCode = await runCli(cliArgs); - process.exit(typeof exitCode === "number" ? exitCode : 0); - } - catch (error) { - process.exit(reportCliFailure(argsWantJson(cliArgs), describeCliError(error), 1)); - } -} -const server = new McpServer({ - name: "loopover-local", - version: packageVersion, -}); -// #4777: register a stdio tool under its loopover_ name. Thin wrapper kept so all 37 call sites -// stay uniform with the rest of this file's registration style. -// Single chokepoint for the #6228 PostHog tool-call telemetry (#6238): every registerStdioTool-registered tool -// routes through here exactly once per invocation, whether it returns or throws. Pure observability -- a -// telemetry failure must never reach the tool caller, so this keeps a defensive try/catch on top of -// recordMcpToolCall's own never-throw guarantee (#6236), mirroring recordMcpToolTelemetry on the remote side -// (#6237). -// -// Reads the opt-in flag HERE, at module scope, on purpose: registerStdioTool's second parameter is the TOOL's -// config and shadows the module-level `config` this resolves from, so a read inside that function would silently -// see the wrong object and never fire. -function recordStdioToolTelemetry(tool, ok, durationMs) { - try { - recordLocalMcpToolCall({ telemetryEnabled: telemetryState().enabled }, { tool, callerType: "local", ok, durationMs }); - } - catch { - // Telemetry must never affect the tool response (#6238). - } -} -function registerStdioTool(name, config, handler) { - server.registerTool(name, config, async (...args) => { - const startedAt = Date.now(); - try { - const result = await handler(...args); - // Mirror the remote's caller-visible outcome (`response.status < 400`): a handler that reports failure by - // returning an error result is not a success, even though it never threw. - recordStdioToolTelemetry(name, result?.isError !== true, Date.now() - startedAt); - return result; - } - catch (error) { - recordStdioToolTelemetry(name, false, Date.now() - startedAt); - throw error; - } - }); -} -registerStdioTool("loopover_get_repo_context", { - description: stdioToolDescription("loopover_get_repo_context"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver repo intelligence.", await apiGet(`${prefix}/intelligence`)); -}); -registerStdioTool("loopover_get_pr_reviewability", { - description: stdioToolDescription("loopover_get_pr_reviewability"), - inputSchema: ownerRepoPullShape, -}, async ({ owner, repo, number }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver PR reviewability.", await apiGet(`${prefix}/pulls/${number}/reviewability`)); -}); -// #6619: CLI mirror of the remote server's loopover_get_pr_ai_review_findings. The route is the single source -// of truth (it delegates to the same loadPrAiReviewFindings the MCP server uses); this tool only resolves the -// author login and proxies. Self-scoped: the route's requireContributorAccess rejects another login's PR. -registerStdioTool("loopover_get_pr_ai_review_findings", { - description: stdioToolDescription("loopover_get_pr_ai_review_findings"), - inputSchema: prAiReviewFindingsShape, -}, async ({ owner, repo, number, login }) => { - const authorLogin = login ?? activeProfile.session?.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!authorLogin) - throw new Error("No GitHub login: pass `login`, log in with `loopover-mcp login`, or set LOOPOVER_LOGIN."); - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver PR AI-review findings.", await apiGet(`${prefix}/pulls/${number}/ai-review-findings?login=${encodeURIComponent(authorLogin)}`)); -}); -registerStdioTool("loopover_get_maintainer_noise", { - description: stdioToolDescription("loopover_get_maintainer_noise"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver maintainer noise report.", await apiGet(`${prefix}/maintainer-noise`)); -}); -registerStdioTool("loopover_get_issue_quality", { - description: stdioToolDescription("loopover_get_issue_quality"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver issue-quality report.", await apiGet(`${prefix}/issue-quality`)); -}); -registerStdioTool("loopover_get_registration_readiness", { - description: stdioToolDescription("loopover_get_registration_readiness"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver registration-readiness report.", await apiGet(`${prefix}/registration-readiness`)); -}); -registerStdioTool("loopover_get_config_recommendation", { - description: stdioToolDescription("loopover_get_config_recommendation"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver config recommendation.", await apiGet(`${prefix}/gittensor-config-recommendation`)); -}); -registerStdioTool("loopover_get_skipped_pr_audit", { - description: stdioToolDescription("loopover_get_skipped_pr_audit"), - inputSchema: skippedPrAuditShape, -}, async ({ repoFullName, reason, since, limit }) => { - const query = new URLSearchParams(); - if (repoFullName) - query.set("repoFullName", repoFullName); - if (reason) - query.set("reason", reason); - if (since) - query.set("since", since); - if (limit != null) - query.set("limit", String(limit)); - const qs = query.toString(); - return toolResult("LoopOver skipped-PR audit trail.", await apiGet(`/v1/app/skipped-pr-audit${qs ? `?${qs}` : ""}`)); -}); -registerStdioTool("loopover_preflight_pr", { - description: stdioToolDescription("loopover_preflight_pr"), - inputSchema: preflightShape, -}, async (input) => toolResult("LoopOver PR preflight.", await apiPost("/v1/preflight/pr", input))); -// #6980: CLI stdio mirror of loopover_explain_review_risk — proxies POST /v1/preflight/review-risk. -registerStdioTool("loopover_explain_review_risk", { - description: stdioToolDescription("loopover_explain_review_risk"), - inputSchema: preflightShape, -}, async (input) => { - const payload = await apiPost("/v1/preflight/review-risk", input); - return toolResult(payload.summary ?? `LoopOver review-risk explanation for ${input.repoFullName}.`, payload); -}); -registerStdioTool("loopover_validate_linked_issue", { - description: stdioToolDescription("loopover_validate_linked_issue"), - inputSchema: validateLinkedIssueShape, -}, async ({ owner, repo, issueNumber, plannedChange }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const body = { issueNumber, ...(plannedChange ? { plannedChange } : {}) }; - return toolResult("LoopOver linked-issue validation.", await apiPost(`${prefix}/validate-linked-issue`, body)); -}); -registerStdioTool("loopover_check_before_start", { - description: stdioToolDescription("loopover_check_before_start"), - inputSchema: checkBeforeStartShape, -}, async ({ owner, repo, issueNumber, title, plannedPaths }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const body = { - ...(issueNumber != null ? { issueNumber } : {}), - ...(title ? { title } : {}), - ...(plannedPaths ? { plannedPaths } : {}), - }; - return toolResult("LoopOver pre-start check.", await apiPost(`${prefix}/check-before-start`, body)); -}); -registerStdioTool("loopover_find_opportunities", { - description: stdioToolDescription("loopover_find_opportunities"), - inputSchema: findOpportunitiesShape, -}, async ({ targets, searchQuery, goalSpec, limit }) => { - const body = { - ...(targets && targets.length > 0 ? { targets } : {}), - ...(searchQuery ? { searchQuery } : {}), - ...(goalSpec ? { goalSpec } : {}), - ...(limit != null ? { limit } : {}), - }; - return toolResult("LoopOver cross-repo opportunities.", await apiPost("/v1/opportunities/find", body)); -}); -registerStdioTool("loopover_retrieve_issue_context", { - description: stdioToolDescription("loopover_retrieve_issue_context"), - inputSchema: issueRagShape, -}, async ({ owner, repo, title, body, labels, topK }) => { - const payload = { - owner, - repo, - title, - ...(body ? { body } : {}), - ...(labels && labels.length > 0 ? { labels } : {}), - ...(topK != null ? { topK } : {}), - }; - return toolResult("LoopOver issue-centric RAG context.", await apiPost("/v1/issue-rag/retrieve", payload)); -}); -// Computed in-process from @loopover/engine (#6268) — matches the remote server's own buildPrTextLint -// call (src/mcp/server.ts) with no API round-trip, so PR-text lint works fully offline. -registerStdioTool("loopover_lint_pr_text", { - description: stdioToolDescription("loopover_lint_pr_text"), - inputSchema: lintPrTextShape, -}, (input) => toolResult("LoopOver PR-text lint.", buildPrTextLint(input))); -// #6269: computed in-process via the extracted engine builder -- no API round-trip, works fully offline. -registerStdioTool("loopover_validate_config", { - description: stdioToolDescription("loopover_validate_config"), - inputSchema: validateConfigShape, -}, (input) => toolResult("LoopOver manifest validation.", buildFocusManifestValidation(input))); -// Computed in-process from @loopover/engine (#6267) — matches the remote server's own buildSlopAssessment -// call (src/mcp/server.ts) and the /v1/lint/slop-risk route's `{ ...assessment, rubric }` shape with no API -// round-trip, so slop-risk self-checks work fully offline. -registerStdioTool("loopover_check_slop_risk", { - description: stdioToolDescription("loopover_check_slop_risk"), - inputSchema: checkSlopRiskShape, -}, (input) => toolResult("LoopOver slop-risk self-check.", { ...buildSlopAssessment(input), rubric: SLOP_RUBRIC_MARKDOWN })); -// #6751: CLI mirror of the remote server's loopover_simulate_open_pr_pressure. Proxies rather than computing -// in-process (like the boundary-tests mirror, #6750): simulateOpenPrPressure lives app-side in -// src/services/open-pr-pressure-scenarios.ts, not in @loopover/engine, so POST /v1/lint/open-pr-pressure stays -// the single source of truth for the ranking. -registerStdioTool("loopover_simulate_open_pr_pressure", { - description: stdioToolDescription("loopover_simulate_open_pr_pressure"), - inputSchema: simulateOpenPrPressureShape, -}, async (input) => toolResult("LoopOver open-PR pressure simulation.", await apiPost("/v1/lint/open-pr-pressure", input))); -// #6750: CLI mirror of the remote server's loopover_suggest_boundary_tests. Unlike its check_slop_risk sibling -// this one PROXIES rather than computing in-process: the builders live app-side (src/signals/ -// boundary-test-generation.ts, which depends on the app's AdvisoryFinding type), not in @loopover/engine, so -// POST /v1/lint/boundary-tests stays the single source of truth for the filtering + finding/spec logic. -registerStdioTool("loopover_suggest_boundary_tests", { - description: stdioToolDescription("loopover_suggest_boundary_tests"), - inputSchema: suggestBoundaryTestsShape, -}, async (input) => toolResult("LoopOver boundary-test suggestion.", await apiPost("/v1/lint/boundary-tests", input))); -// Computed in-process from @loopover/engine (#6749) — the same buildTestEvidenceReport the remote server -// (src/mcp/server.ts) and the /v1/lint/test-evidence route both call, so all three surfaces return a -// byte-identical verdict and coverage self-checks work fully offline. -registerStdioTool("loopover_check_test_evidence", { - description: stdioToolDescription("loopover_check_test_evidence"), - inputSchema: checkTestEvidenceShape, -}, (input) => toolResult("LoopOver test-evidence check.", buildTestEvidenceReport(input))); -// Computed in-process from @loopover/engine (#6754) — the same pure evaluateEscalation the remote server -// (src/mcp/server.ts) and the /v1/loop/evaluate-escalation route both call, so all three surfaces return a -// byte-identical decision for identical input, and escalation checks work fully offline. -registerStdioTool("loopover_evaluate_escalation", { - description: stdioToolDescription("loopover_evaluate_escalation"), - inputSchema: evaluateEscalationShape, -}, (input) => toolResult("LoopOver escalation decision.", evaluateEscalation(input))); -// Computed in-process from @loopover/engine (#6752) — the same pure buildResultsPayload the remote server -// (src/mcp/server.ts) and the /v1/loop/results-payload route both call, so all three surfaces return an -// identical payload for identical input, and results composition works fully offline. -registerStdioTool("loopover_build_results_payload", { - description: stdioToolDescription("loopover_build_results_payload"), - inputSchema: resultsPayloadShape, -}, (input) => toolResult("LoopOver loop results payload.", buildResultsPayload(input))); -// Computed in-process from @loopover/engine (#6753) — the same pure buildProgressSnapshot the remote server -// (src/mcp/server.ts) and the /v1/loop/progress-snapshot route both call, so all three surfaces return an -// identical snapshot for identical input, and progress composition works fully offline. -registerStdioTool("loopover_build_progress_snapshot", { - description: stdioToolDescription("loopover_build_progress_snapshot"), - inputSchema: buildProgressSnapshotShape, -}, (input) => toolResult("LoopOver loop progress snapshot.", buildProgressSnapshot(input))); -// Computed in-process from @loopover/engine (#6755) — the same pure validateIdeaSubmission/buildTaskGraph the -// remote server (src/mcp/server.ts) and the /v1/loop/intake-idea route both call, reproducing the tool's -// handler exactly so all three surfaces return an identical payload for identical input, fully offline. -registerStdioTool("loopover_intake_idea", { - description: stdioToolDescription("loopover_intake_idea"), - inputSchema: intakeIdeaShape, -}, (input) => { - const validated = validateIdeaSubmission(input); - if (!validated.ok) - return toolResult(`Invalid idea submission: ${validated.errors.join(", ")}.`, { ok: false, errors: validated.errors }); - const taskGraph = buildTaskGraph(validated.idea, input.decomposition); - return toolResult(`Task-graph verdict: ${taskGraph.rubric.verdict} across ${taskGraph.issues.length} issue(s).`, { - ok: true, - verdict: taskGraph.rubric.verdict, - taskGraph, - }); -}); -// Computed in-process from @loopover/engine (#6756) — the same pure validateIdeaSubmission/buildTaskGraph/ -// buildClaimPlan the remote server (src/mcp/server.ts) and the /v1/loop/plan-idea-claims route both call, -// reproducing the tool's handler exactly so all three surfaces return an identical payload for identical -// input, fully offline. -registerStdioTool("loopover_plan_idea_claims", { - description: stdioToolDescription("loopover_plan_idea_claims"), - inputSchema: intakeIdeaShape, -}, (input) => { - const validated = validateIdeaSubmission(input); - if (!validated.ok) - return toolResult(`Invalid idea submission: ${validated.errors.join(", ")}.`, { ok: false, errors: validated.errors }); - const graph = buildTaskGraph(validated.idea, input.decomposition); - const claimPlan = buildClaimPlan(graph, validated.idea.targetRepo); - return toolResult(`Claim plan: ${claimPlan.claimable.length} claimable, ${claimPlan.deferred.length} deferred, ${claimPlan.skipped.length} skipped.`, { ok: true, verdict: claimPlan.graphVerdict, claimPlan }); -}); -registerStdioTool("loopover_check_issue_slop", { - description: stdioToolDescription("loopover_check_issue_slop"), - inputSchema: checkIssueSlopShape, -}, async (input) => toolResult("LoopOver issue-slop self-check.", await apiPost("/v1/lint/issue-slop", input))); -// Computed in-process from @loopover/engine (#6150) — matches the remote server's own -// computeLocalScorerTokens call (src/mcp/server.ts) with no API round-trip, so token scoring works fully -// offline. -registerStdioTool("loopover_run_local_scorer", { - description: stdioToolDescription("loopover_run_local_scorer"), - inputSchema: runLocalScorerShape, -}, (input) => toolResult("LoopOver local token scores.", computeLocalScorerTokens(input))); -// Computed in-process (#6150) — matches the remote server's own buildPlanDag call (src/mcp/server.ts) -// with no API round-trip; the plan-DAG logic itself is hand-duplicated above (see its own comment). -registerStdioTool("loopover_build_plan", { - description: stdioToolDescription("loopover_build_plan"), - inputSchema: buildPlanShape, -}, (input) => toolResult("LoopOver plan built.", planView(buildPlanDag(input.steps)))); -registerStdioTool("loopover_plan_status", { - description: stdioToolDescription("loopover_plan_status"), - inputSchema: planStatusShape, -}, (input) => toolResult("LoopOver plan status.", planView(input.plan))); -registerStdioTool("loopover_record_step_result", { - description: stdioToolDescription("loopover_record_step_result"), - inputSchema: recordStepResultShape, -}, (input) => toolResult("LoopOver plan step result recorded.", planView(applyStepResult(input.plan, input.stepId, { outcome: input.outcome, ...(input.error !== undefined ? { error: input.error } : {}) })))); -// Metadata-only proxy to the same route the branch-analysis tools already use (#6150) — that route computes -// predictedGate via buildPredictedGateVerdict (the identical logic the remote loopover_predict_gate tool -// uses) and returns it as a top-level field; no local git/workspace context is needed for this shape. -registerStdioTool("loopover_predict_gate", { - description: stdioToolDescription("loopover_predict_gate"), - inputSchema: predictGateShape, -}, async (input) => { - const body = { - login: input.login, - repoFullName: `${input.owner}/${input.repo}`, - title: input.title, - ...(input.body !== undefined ? { body: input.body } : {}), - ...(input.labels !== undefined ? { labels: input.labels } : {}), - ...(input.linkedIssues !== undefined ? { linkedIssues: input.linkedIssues } : {}), - ...(input.changedPaths !== undefined ? { changedFiles: input.changedPaths.map((path) => ({ path })) } : {}), - }; - const result = await apiPost("/v1/local/branch-analysis", body); - return toolResult(`LoopOver predicted gate for ${input.owner}/${input.repo}.`, result.predictedGate); -}); -// #6740: CLI stdio mirror of loopover_explain_gate_disposition — same branch-analysis fetch as predict_gate, -// then the shared pure buildGateDispositions reshaper (now exported from @loopover/engine) runs locally. -registerStdioTool("loopover_explain_gate_disposition", { - description: stdioToolDescription("loopover_explain_gate_disposition"), - inputSchema: predictGateShape, -}, async (input) => { - const body = { - login: input.login, - repoFullName: `${input.owner}/${input.repo}`, - title: input.title, - ...(input.body !== undefined ? { body: input.body } : {}), - ...(input.labels !== undefined ? { labels: input.labels } : {}), - ...(input.linkedIssues !== undefined ? { linkedIssues: input.linkedIssues } : {}), - ...(input.changedPaths !== undefined ? { changedFiles: input.changedPaths.map((path) => ({ path })) } : {}), - }; - const result = await apiPost("/v1/local/branch-analysis", body); - const verdict = result.predictedGate; - const dispositions = buildGateDispositions(verdict ?? { blockers: [], warnings: [] }); - const blocking = dispositions.filter((disposition) => disposition.status === "block").length; - return toolResult(`Gate disposition for ${input.owner}/${input.repo} under the ${verdict?.pack ?? "unknown"} pack: ${verdict?.conclusion ?? "unknown"} — ${blocking} blocking rule(s), ${dispositions.length - blocking} advisory.`, { conclusion: verdict?.conclusion, pack: verdict?.pack, dispositions }); -}); -registerStdioTool("loopover_preflight_local_diff", { - description: stdioToolDescription("loopover_preflight_local_diff"), - inputSchema: localDiffShape, -}, async (input) => { - const workspaceInput = await withClientWorkspaceRoots(input); - const diff = collectLocalDiff(workspaceInput.cwd, input.baseRef, workspaceInput.workspaceRoots); - const body = { - repoFullName: input.repoFullName, - contributorLogin: input.contributorLogin, - title: input.title ?? diff.title, - body: input.body, - labels: input.labels, - linkedIssues: input.linkedIssues, - tests: input.tests, - authorAssociation: input.authorAssociation, - commitMessage: input.commitMessage ?? diff.commitMessage, - changedFiles: diff.changedFiles, - testFiles: diff.testFiles, - changedLineCount: diff.changedLineCount, - }; - return toolResult("LoopOver local diff preflight.", await apiPost("/v1/preflight/local-diff", body)); -}); -registerStdioTool("loopover_get_registry_changes", { - description: stdioToolDescription("loopover_get_registry_changes"), - inputSchema: {}, -}, async () => toolResult("LoopOver registry changes.", await apiGet("/v1/registry/changes"))); -// #6736: CLI mirror of the public loopover_get_bounty_advisory tool. Proxies the same unauthenticated -// GET /v1/bounties/:id/advisory the remote tool wraps -- no owner/repo, just the cached-bounty id. -registerStdioTool("loopover_get_bounty_advisory", { - description: stdioToolDescription("loopover_get_bounty_advisory"), - inputSchema: bountyAdvisoryShape, -}, async ({ id }) => toolResult("LoopOver bounty advisory.", await apiGet(`/v1/bounties/${encodeURIComponent(id)}/advisory`))); -registerStdioTool("loopover_get_upstream_drift", { - description: stdioToolDescription("loopover_get_upstream_drift"), - inputSchema: {}, -}, async () => toolResult("LoopOver upstream drift status.", await apiGet("/v1/upstream/drift"))); -registerStdioTool("loopover_get_label_audit", { - description: stdioToolDescription("loopover_get_label_audit"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const intelligence = await apiGet(`${prefix}/intelligence`); - return toolResult("LoopOver label audit.", { - repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`, - generatedAt: intelligence?.generatedAt, - labelAudit: intelligence?.labelAudit ?? null, - }); -}); -// #6739: CLI mirror of the remote server's loopover_get_maintainer_lane. maintainerLane ships in the same -// buildRepoIntelligenceResponse payload the sibling loopover_get_label_audit already GETs, so this is a thin -// extraction over that identical route rather than a new fetch shape. -registerStdioTool("loopover_get_maintainer_lane", { - description: stdioToolDescription("loopover_get_maintainer_lane"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const intelligence = await apiGet(`${prefix}/intelligence`); - return toolResult("LoopOver maintainer lane.", { - repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`, - generatedAt: intelligence?.generatedAt, - maintainerLane: intelligence?.maintainerLane ?? null, - }); -}); -registerStdioTool("loopover_get_burden_forecast", { - description: stdioToolDescription("loopover_get_burden_forecast"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const intelligence = await apiGet(`${prefix}/intelligence`); - return toolResult("LoopOver burden forecast.", { - repoFullName: intelligence?.repoFullName ?? `${owner}/${repo}`, - generatedAt: intelligence?.generatedAt, - burdenForecast: intelligence?.burdenForecast ?? null, - burdenForecastFreshness: intelligence?.burdenForecastFreshness ?? null, - }); -}); -// #6734: CLI stdio mirror of loopover_get_repo_outcome_patterns — thin GET proxy of the already-public -// /v1/repos/:owner/:repo/outcome-patterns route (same ownerRepoShape + apiGet pattern as maintainer_noise). -registerStdioTool("loopover_get_repo_outcome_patterns", { - description: stdioToolDescription("loopover_get_repo_outcome_patterns"), - inputSchema: ownerRepoShape, -}, async ({ owner, repo }) => { - const prefix = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - return toolResult("LoopOver repo outcome patterns.", await apiGet(`${prefix}/outcome-patterns`)); -}); -registerStdioTool("loopover_preview_local_pr_score", { - description: stdioToolDescription("loopover_preview_local_pr_score"), - inputSchema: localScoreShape, -}, async (input) => toolResult("LoopOver private local PR scoring preview.", await previewLocalScore(await withClientWorkspaceRoots(input)))); -// Shared by loopover_explain_score_breakdown and loopover_get_eligibility_plan (#6621): both resolve the same -// local branch/diff metadata into the /v1/scoring request body — only the endpoint they POST it to differs, so -// the assembly lives here once rather than in two drifting copies. -function buildLocalScoreRequestBody(workspaceInput, contributorLogin) { - const workspace = resolveWorkspaceCwd(workspaceInput); - const diff = collectLocalDiff(workspace.cwd, workspaceInput.baseRef, workspaceInput.workspaceRoots); - const branchPayload = buildBranchAnalysisPayload({ - ...workspaceInput, - login: contributorLogin, - cwd: workspace.cwd, - repoFullName: workspaceInput.repoFullName, - baseRef: workspaceInput.baseRef, - }); - const upstreamPreview = branchPayload.localScorerStatus; - const estimatedSourceLines = workspaceInput.sourceLines ?? Math.max(1, diff.changedLineCount - diff.testFiles.length); - return { - repoFullName: workspaceInput.repoFullName, - targetType: "local_diff", - targetKey: workspaceInput.targetKey ?? localDiffTargetKey(branchPayload, workspaceInput.baseRef), - contributorLogin, - labels: workspaceInput.labels, - linkedIssueMode: workspaceInput.linkedIssueMode, - sourceTokenScore: workspaceInput.sourceTokenScore ?? estimatedSourceLines, - sourceLines: estimatedSourceLines, - totalTokenScore: workspaceInput.totalTokenScore ?? diff.changedLineCount, - testTokenScore: diff.testFiles.length, - openPrCount: workspaceInput.openPrCount, - credibility: workspaceInput.credibility, - changesRequestedCount: workspaceInput.changesRequestedCount, - pendingMergedPrCount: workspaceInput.pendingMergedPrCount, - pendingClosedPrCount: workspaceInput.pendingClosedPrCount, - approvedPrCount: workspaceInput.approvedPrCount, - expectedOpenPrCountAfterMerge: workspaceInput.expectedOpenPrCountAfterMerge, - projectedCredibility: workspaceInput.projectedCredibility, - scenarioNotes: workspaceInput.scenarioNotes, - branchEligibility: workspaceInput.branchEligibility, - metadataOnly: !upstreamPreview.ok, - }; -} -registerStdioTool("loopover_explain_score_breakdown", { - description: stdioToolDescription("loopover_explain_score_breakdown"), - inputSchema: localScoreShape, -}, async (input) => { - const workspaceInput = await withClientWorkspaceRoots(input); - const contributorLogin = workspaceInput.contributorLogin ?? activeProfile.session?.login; - if (!contributorLogin) - throw new Error("contributorLogin is required for score breakdown."); - const body = buildLocalScoreRequestBody(workspaceInput, contributorLogin); - return toolResult("LoopOver private score breakdown.", await apiPost("/v1/scoring/explain-breakdown", body)); -}); -registerStdioTool("loopover_get_eligibility_plan", { - description: stdioToolDescription("loopover_get_eligibility_plan"), - inputSchema: localScoreShape, -}, async (input) => { - const workspaceInput = await withClientWorkspaceRoots(input); - const contributorLogin = workspaceInput.contributorLogin ?? activeProfile.session?.login; - if (!contributorLogin) - throw new Error("contributorLogin is required for the eligibility plan."); - const body = buildLocalScoreRequestBody(workspaceInput, contributorLogin); - return toolResult("LoopOver private eligibility plan.", await apiPost("/v1/scoring/eligibility-plan", body)); -}); -registerStdioTool("loopover_get_decision_pack", { - description: stdioToolDescription("loopover_get_decision_pack"), - inputSchema: loginShape, -}, async ({ login }) => { - const payload = await getDecisionPackWithCache(login); - return toolResult(decisionPackToolSummary(login, payload), payload); -}); -registerStdioTool("loopover_explain_repo_decision", { - description: stdioToolDescription("loopover_explain_repo_decision"), - inputSchema: loginRepoShape, -}, async ({ login, owner, repo }) => { - const payload = await getRepoDecisionWithCache(login, owner, repo); - return toolResult(repoDecisionToolSummary(login, `${owner}/${repo}`, payload), payload); -}); -registerStdioTool("loopover_monitor_open_prs", { - description: stdioToolDescription("loopover_monitor_open_prs"), - inputSchema: loginShape, -}, async ({ login }) => { - const payload = await getOpenPrMonitor(login); - return toolResult(openPrMonitorToolSummary(login, payload), payload); -}); -registerStdioTool("loopover_pr_outcome", { - description: stdioToolDescription("loopover_pr_outcome"), - inputSchema: { - login: z.string().min(1), - limit: z.number().int().positive().max(100).optional(), - }, -}, async ({ login, limit }) => { - const payload = await getPrOutcomes(login, limit); - return toolResult(prOutcomesToolSummary(login, payload), payload); -}); -registerStdioTool("loopover_compare_pr_variants", { - description: stdioToolDescription("loopover_compare_pr_variants"), - inputSchema: variantsShape, -}, async ({ variants }) => { - const roots = await clientWorkspaceRoots(); - const previews = []; - for (const variant of variants) - previews.push(await previewLocalScore(withWorkspaceRoots({ ...variant, targetKey: variant.targetKey ?? `variant:${previews.length + 1}` }, roots))); - previews.sort((left, right) => Number(right?.remotePreview?.result?.effectiveEstimatedScore ?? right?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left?.remotePreview?.result?.effectiveEstimatedScore ?? left?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0)); - return toolResult("LoopOver PR variant comparison.", { variants: previews }); -}); -registerStdioTool("loopover_local_status", { - description: stdioToolDescription("loopover_local_status"), - inputSchema: { - cwd: z.string().optional(), - baseRef: z.string().optional(), - repoFullName: z.string().min(3).optional(), - }, -}, async (input) => { - let git = null; - const workspaceInput = await withClientWorkspaceRoots(input); - try { - git = collectLocalBranchMetadata({ cwd: workspaceInput.cwd, baseRef: input.baseRef, repoFullName: input.repoFullName, login: "local", workspaceRoots: workspaceInput.workspaceRoots }); - } - catch (error) { - git = { error: error instanceof Error ? error.message : "local_status_failed" }; - } - return toolResult("LoopOver local MCP status.", { - apiUrl, - package: { - name: packageName, - version: packageVersion, - }, - hasToken: Boolean(getApiToken()), - profile: profilePublicState(activeProfileName), - authLogin: activeProfile.session?.login ?? null, - sessionExpiresAt: activeProfile.session?.expiresAt ?? null, - sourceUploadDefault: false, - sourceUploadSupported: false, - workspaceRoots: workspaceRootStatus(workspaceInput.workspaceRoots), - git, - }); -}); -registerStdioTool("loopover_preflight_current_branch", { - description: stdioToolDescription("loopover_preflight_current_branch"), - inputSchema: currentBranchShape, -}, async (input) => { - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(input)); - return toolResult("LoopOver current-branch preflight.", { - local: result.local, - preflight: result.analysis.preflight, - prPacket: result.analysis.prPacket, - workspaceIntelligence: publicSafeWorkspaceIntelligence(result.analysis.workspaceIntelligence), - }); -}); -registerStdioTool("loopover_review_pr_before_push", { - description: stdioToolDescription("loopover_review_pr_before_push"), - inputSchema: currentBranchShape, -}, async (input) => toolResult("LoopOver pre-PR review.", await reviewLocalPr(await withClientWorkspaceRoots(input)))); -registerStdioTool("loopover_preview_current_branch_score", { - description: stdioToolDescription("loopover_preview_current_branch_score"), - inputSchema: currentBranchShape, -}, async (input) => { - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(input)); - return toolResult("LoopOver current-branch private score preview.", { - local: result.local, - scorePreview: result.analysis.scorePreview, - scenarioScorePreview: result.analysis.scenarioScorePreview, - scoreBlockers: result.analysis.scoreBlockers, - recommendedRerunCondition: result.analysis.recommendedRerunCondition, - }); -}); -registerStdioTool("loopover_rank_local_next_actions", { - description: stdioToolDescription("loopover_rank_local_next_actions"), - inputSchema: currentBranchShape, -}, async (input) => { - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(input)); - return toolResult("LoopOver local next-action ranking.", { local: result.local, nextActions: result.analysis.nextActions, rewardRisk: result.analysis.rewardRisk, recommendedRerunCondition: result.analysis.recommendedRerunCondition }); -}); -registerStdioTool("loopover_explain_local_blockers", { - description: stdioToolDescription("loopover_explain_local_blockers"), - inputSchema: currentBranchShape, -}, async (input) => { - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(input)); - return toolResult("LoopOver local blocker explanation.", { - local: result.local, - scoreBlockers: result.analysis.scoreBlockers, - branchQualityBlockers: result.analysis.branchQualityBlockers, - accountStateBlockers: result.analysis.accountStateBlockers, - baseFreshness: result.analysis.baseFreshness, - localFindings: result.analysis.localFindings, - recommendedRerunCondition: result.analysis.recommendedRerunCondition, - }); -}); -registerStdioTool("loopover_remediation_plan", { - description: stdioToolDescription("loopover_remediation_plan"), - inputSchema: currentBranchShape, -}, async (input) => { - const workspaceInput = await withClientWorkspaceRoots(input); - const payload = buildBranchAnalysisPayload({ ...workspaceInput, cwd: resolveWorkspaceCwd(workspaceInput).cwd }); - const { localScorerStatus: _localScorerStatus, ...body } = payload; - return toolResult("LoopOver remediation plan.", await apiPost("/v1/local/remediation-plan", body)); -}); -registerStdioTool("loopover_prepare_pr_packet", { - description: stdioToolDescription("loopover_prepare_pr_packet"), - inputSchema: currentBranchShape, -}, async (input) => { - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(input)); - return toolResult("LoopOver public-safe PR packet.", { local: result.local, prPacket: result.analysis.prPacket }); -}); -// #6741: CLI stdio mirror of loopover_draft_pr_body — same analyzeCurrentBranch fetch as prepare_pr_packet, -// then the shared pure buildPublicPrBodyDraft (now exported from @loopover/engine) runs locally. -const draftPrBodyShape = { - ...currentBranchShape, - format: z.enum(["json", "markdown"]).optional(), -}; -registerStdioTool("loopover_draft_pr_body", { - description: stdioToolDescription("loopover_draft_pr_body"), - inputSchema: draftPrBodyShape, -}, async (input) => { - const { format, ...branchInput } = input; - const result = await analyzeCurrentBranch(await withClientWorkspaceRoots(branchInput)); - const draft = buildPublicPrBodyDraft(result.analysis); - if (format === "markdown") { - return toolResult(`Public-safe PR body draft for ${draft.repoFullName} (markdown).\n\n${draft.markdown}`, { - markdown: draft.markdown, - title: draft.title, - repoFullName: draft.repoFullName, - sourceUploadDisabled: true, - }); - } - return toolResult(`Public-safe PR body draft for ${draft.repoFullName} (metadata only; internal analysis context omitted).\n\n${draft.markdown}`, draft); -}); -registerStdioTool("loopover_compare_local_variants", { - description: stdioToolDescription("loopover_compare_local_variants"), - inputSchema: currentBranchVariantsShape, -}, async ({ variants }) => { - const roots = await clientWorkspaceRoots(); - const analyses = []; - for (const variant of variants) - analyses.push(await analyzeCurrentBranch(withWorkspaceRoots(variant, roots))); - analyses.sort((left, right) => Number(right.analysis.nextActions?.[0]?.priorityScore ?? 0) - Number(left.analysis.nextActions?.[0]?.priorityScore ?? 0) || - Number(right.analysis.scorePreview?.effectiveEstimatedScore ?? right.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left.analysis.scorePreview?.effectiveEstimatedScore ?? left.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0)); - return toolResult("LoopOver local variant comparison.", { - variants: analyses.map((entry) => ({ - local: entry.local, - preflightStatus: entry.analysis.preflight.status, - scoreBlockers: entry.analysis.scoreBlockers, - topAction: entry.analysis.nextActions?.[0] ?? null, - prPacket: entry.analysis.prPacket, - })), - }); -}); -registerStdioTool("loopover_agent_plan_next_work", { - description: stdioToolDescription("loopover_agent_plan_next_work"), - inputSchema: agentPlanShape, -}, async (input) => toolResult(`LoopOver base-agent plan for ${input.login}.`, await apiPost("/v1/agent/plan-next-work", input))); -registerStdioTool("loopover_agent_start_run", { - description: stdioToolDescription("loopover_agent_start_run"), - inputSchema: agentRunShape, -}, async (input) => toolResult(`Queued LoopOver base-agent run for ${input.actorLogin}.`, await apiPost("/v1/agent/runs", { - objective: input.objective, - actorLogin: input.actorLogin, - surface: "mcp", - target: stripUndefined({ - repoFullName: input.targetRepoFullName, - pullNumber: input.targetPullNumber, - issueNumber: input.targetIssueNumber, - }), -}))); -registerStdioTool("loopover_agent_get_run", { - description: stdioToolDescription("loopover_agent_get_run"), - inputSchema: agentRunIdShape, -}, async ({ runId }) => toolResult(`LoopOver base-agent run ${runId}.`, await apiGet(`/v1/agent/runs/${encodeURIComponent(runId)}`))); -registerStdioTool("loopover_agent_explain_next_action", { - description: stdioToolDescription("loopover_agent_explain_next_action"), - inputSchema: agentPlanShape, -}, async (input) => { - const result = await apiPost("/v1/agent/explain-blockers", input); - return toolResult(`LoopOver base-agent next-action explanation for ${input.login}.`, { - ...result, - topAction: result.actions?.[0] ?? null, - }); -}); -registerStdioTool("loopover_agent_prepare_pr_packet", { - description: stdioToolDescription("loopover_agent_prepare_pr_packet"), - inputSchema: currentBranchShape, -}, async (input) => toolResult("LoopOver base-agent public-safe PR packet.", await agentPreparePrPacket(await withClientWorkspaceRoots(input)))); -// ── Output schemas for structured tool responses (#291) ────────────────────── -const repoContextOutputSchema = { - type: "object", - properties: { - repoFullName: { type: "string" }, - lane: { type: "string" }, - primaryLanguage: { type: ["string", "null"] }, - openIssueCount: { type: "number" }, - openPrCount: { type: "number" }, - }, - additionalProperties: true, -}; -const preflightOutputSchema = { - type: "object", - properties: { - status: { type: "string", enum: ["pass", "warn", "fail", "unknown"] }, - signals: { type: "array", items: { type: "object" } }, - summary: { type: "string" }, - }, - additionalProperties: true, -}; -const decisionPackOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - decisions: { type: "array", items: { type: "object" } }, - cachedAt: { type: ["string", "null"] }, - }, - additionalProperties: true, -}; -const localStatusOutputSchema = { - type: "object", - properties: { - apiUrl: { type: "string" }, - package: { type: "object", properties: { name: { type: "string" }, version: { type: "string" } }, additionalProperties: true }, - hasToken: { type: "boolean" }, - profile: { type: "object", additionalProperties: true }, - authLogin: { type: ["string", "null"] }, - sessionExpiresAt: { type: ["string", "null"] }, - sourceUploadDefault: { type: "boolean" }, - sourceUploadSupported: { type: "boolean" }, - git: { type: "object", additionalProperties: true }, - }, - additionalProperties: true, -}; -const agentPlanOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - actions: { type: "array", items: { type: "object" } }, - topAction: { type: ["object", "null"] }, - }, - additionalProperties: true, -}; -// Attach outputSchema to key tools via registerTool with zod output schemas. -// All other tools continue to return unschematized text+structured content. -registerStdioTool("loopover_local_status_structured", { - description: stdioToolDescription("loopover_local_status_structured"), - inputSchema: { - cwd: z.string().optional(), - baseRef: z.string().optional(), - repoFullName: z.string().min(3).optional(), - }, - outputSchema: z.object({ - apiUrl: z.string(), - package: z.object({ name: z.string(), version: z.string() }), - hasToken: z.boolean(), - profile: z.record(z.string(), z.unknown()), - authLogin: z.string().nullable(), - sessionExpiresAt: z.string().nullable(), - sourceUploadDefault: z.boolean(), - sourceUploadSupported: z.boolean(), - git: z.record(z.string(), z.unknown()), - }), -}, async (input) => { - let git = null; - const workspaceInput = await withClientWorkspaceRoots(input); - try { - git = collectLocalBranchMetadata({ cwd: workspaceInput.cwd, baseRef: input.baseRef, repoFullName: input.repoFullName, login: "local", workspaceRoots: workspaceInput.workspaceRoots }); - } - catch (error) { - git = { error: error instanceof Error ? error.message : "local_status_failed" }; - } - const data = { - apiUrl, - package: { name: packageName, version: packageVersion }, - hasToken: Boolean(getApiToken()), - profile: profilePublicState(activeProfileName), - authLogin: activeProfile.session?.login ?? null, - sessionExpiresAt: activeProfile.session?.expiresAt ?? null, - sourceUploadDefault: false, - sourceUploadSupported: false, - git: git ?? {}, - }; - return { content: [{ type: "text", text: `LoopOver local MCP status.\n\n${JSON.stringify(data, null, 2)}` }], structuredContent: data }; -}); -registerStdioTool("loopover_feasibility_gate", { - description: stdioToolDescription("loopover_feasibility_gate"), - inputSchema: feasibilityGateShape, -}, async ({ claimStatus, duplicateClusterRisk, issueStatus, found, repoFullName, issueNumber }) => { - const ledgerClaimStatus = await resolveLedgerClaimStatus(repoFullName, issueNumber); - return toolResult("LoopOver feasibility gate.", buildFeasibilityVerdict({ claimStatus: ledgerClaimStatus ?? claimStatus, duplicateClusterRisk, issueStatus, found })); -}); -// ── #6152 maintain surface: the REST calls maintainCli already makes, exposed as tools ─────────────────────── -// -// These five mirror remote tools that have existed since #6087 but were never registered locally, so an agent on -// the stdio server had to shell out to the `maintain` CLI to reach them. Each one calls the same endpoint its -// CLI subcommand calls, through the same apiGet/apiPost/apiFetch client (auth, timeouts, and error shaping come -// from there) -- no new HTTP paths, and no behaviour the CLI doesn't already have. -/** `/v1/repos/:owner/:repo` for a tool's owner+repo input, matching maintainCli's own repoBase. */ -function toolRepoBase(owner, repo) { - return `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; -} -registerStdioTool("loopover_list_pending_actions", { - description: stdioToolDescription("loopover_list_pending_actions"), - inputSchema: listPendingActionsShape, -}, async ({ owner, repo }) => { - const payload = await apiGet(`${toolRepoBase(owner, repo)}/agent/pending-actions`); - return toolResult(`Agent approval queue for ${owner}/${repo}: ${(payload.pendingActions ?? []).length} pending.`, payload); -}); -registerStdioTool("loopover_decide_pending_action", { - description: stdioToolDescription("loopover_decide_pending_action"), - inputSchema: decidePendingActionShape, -}, async ({ owner, repo, id, decision }) => { - const payload = await apiPost(`${toolRepoBase(owner, repo)}/agent/pending-actions/${encodeURIComponent(id)}/${decision}`, {}); - return toolResult(`${decision === "accept" ? "Accepted" : "Rejected"} ${id}: ${payload.status ?? "ok"}.`, payload); -}); -registerStdioTool("loopover_set_agent_paused", { - description: stdioToolDescription("loopover_set_agent_paused"), - inputSchema: setAgentPausedShape, -}, async ({ owner, repo, paused }) => { - const payload = await apiFetch(`${toolRepoBase(owner, repo)}/settings`, { method: "PUT", body: JSON.stringify({ agentPaused: paused }) }); - return toolResult(`Agent actions ${paused ? "paused" : "resumed"} for ${owner}/${repo}.`, payload); -}); -registerStdioTool("loopover_set_action_autonomy", { - description: stdioToolDescription("loopover_set_action_autonomy"), - inputSchema: setActionAutonomyShape, -}, async ({ owner, repo, action, level }) => { - // Read-merge-write, exactly as `maintain set-level` does it: PUT /settings replaces the whole autonomy map, - // so sending only this class would silently clear every other one. - const base = toolRepoBase(owner, repo); - const current = await apiGet(`${base}/settings`); - const autonomy = { ...(current.autonomy ?? {}), [action]: level }; - const payload = await apiFetch(`${base}/settings`, { method: "PUT", body: JSON.stringify({ autonomy }) }); - return toolResult(`Set ${action} autonomy to ${level} for ${owner}/${repo}.`, payload); -}); -registerStdioTool("loopover_get_gate_precision", { - description: stdioToolDescription("loopover_get_gate_precision"), - inputSchema: gatePrecisionShape, -}, async ({ owner, repo, windowDays }) => { - // The schema already rejects a non-positive windowDays, so an omitted window is the only way to full history - // -- matching the route's own behaviour when ?windowDays is absent. - const query = windowDays ? `?windowDays=${encodeURIComponent(windowDays)}` : ""; - const payload = await apiGet(`${toolRepoBase(owner, repo)}/gate-precision${query}`); - return toolResult(`Gate precision for ${owner}/${repo}.`, payload); -}); -// ── Write-tools (#6149): pure LOCAL-execution spec builders. loopover NEVER performs the write -- each tool -// returns a spec the caller runs with its OWN gh creds. Brings the local stdio server to parity with the -// miner-auto-dev profile's recommendedTools, using the same @loopover/engine builders as the remote server. -function localWriteSpecResult(spec) { - return toolResult(`${spec.action}: ${spec.description} ${spec.boundary}`, spec); -} -registerStdioTool("loopover_open_pr", { - description: stdioToolDescription("loopover_open_pr"), - inputSchema: openPrShape, -}, (input) => localWriteSpecResult(buildOpenPrSpec(input))); -registerStdioTool("loopover_file_issue", { - description: stdioToolDescription("loopover_file_issue"), - inputSchema: fileIssueShape, -}, (input) => localWriteSpecResult(buildFileIssueSpec(input))); -registerStdioTool("loopover_apply_labels", { - description: stdioToolDescription("loopover_apply_labels"), - inputSchema: applyLabelsShape, -}, (input) => localWriteSpecResult(buildApplyLabelsSpec(input))); -registerStdioTool("loopover_post_eligibility_comment", { - description: stdioToolDescription("loopover_post_eligibility_comment"), - inputSchema: postEligibilityCommentShape, -}, (input) => localWriteSpecResult(buildPostEligibilityCommentSpec(input))); -registerStdioTool("loopover_create_branch", { - description: stdioToolDescription("loopover_create_branch"), - inputSchema: createBranchShape, -}, (input) => localWriteSpecResult(buildCreateBranchSpec(input))); -registerStdioTool("loopover_delete_branch", { - description: stdioToolDescription("loopover_delete_branch"), - inputSchema: deleteBranchShape, -}, (input) => localWriteSpecResult(buildDeleteBranchSpec(input))); -registerStdioTool("loopover_generate_tests", { - description: stdioToolDescription("loopover_generate_tests"), - inputSchema: testGenShape, -}, (input) => localWriteSpecResult(buildTestGenSpec(input))); -registerStdioTool("loopover_file_follow_up_issue", { - description: stdioToolDescription("loopover_file_follow_up_issue"), - inputSchema: followUpIssueShape, -}, (input) => localWriteSpecResult(buildFollowUpIssueSpec(input))); -registerStdioTool("loopover_close_pr", { - description: stdioToolDescription("loopover_close_pr"), - inputSchema: closePrShape, -}, (input) => localWriteSpecResult(buildClosePrSpec(input))); -// ── Resources: decision-pack, doctor, compatibility, changelog (#292) ───────── -server.registerResource("loopover_changelog", "loopover://changelog", { - title: "LoopOver MCP Changelog", - description: "Current CHANGELOG.md for the installed loopover-mcp package.", - mimeType: "text/markdown", -}, async () => { - let text; - try { - text = readFileSync(changelogPath, "utf8"); - } - catch { - text = "Changelog not available."; - } - return { contents: [{ uri: "loopover://changelog", mimeType: "text/markdown", text }] }; -}); -server.registerResource("loopover_compatibility", "loopover://compatibility", { - title: "LoopOver API Compatibility", - description: "Current API compatibility state: version, supported methods, and any deprecation notices.", - mimeType: "application/json", -}, async () => { - let data; - try { - data = await apiGet(compatibilityPath); - } - catch { - data = { status: "unavailable", currentApiVersion, packageVersion }; - } - return { contents: [{ uri: "loopover://compatibility", mimeType: "application/json", text: JSON.stringify(data, null, 2) }] }; -}); -// #6620: mirror the two remote static-document MCP resources over the local stdio server, proxying the new -// unauthenticated REST routes the same way loopover_compatibility proxies /v1/mcp/compatibility. Reuse the exact -// URIs the remote server registers. -server.registerResource("loopover_finding_taxonomy", "loopover://finding-taxonomy", { - title: "LoopOver Finding Taxonomy", - description: "Static taxonomy of AI-review finding categories and the severity ladder.", - mimeType: "application/json", -}, async () => { - let data; - try { - data = await apiGet(findingTaxonomyPath); - } - catch { - data = { status: "unavailable" }; - } - return { contents: [{ uri: "loopover://finding-taxonomy", mimeType: "application/json", text: JSON.stringify(data, null, 2) }] }; -}); -server.registerResource("loopover_enrichment_analyzers", "loopover://enrichment-analyzers", { - title: "LoopOver Enrichment Analyzers", - description: "Static taxonomy of REES enrichment analyzers: names, categories, and cost classes.", - mimeType: "application/json", -}, async () => { - let data; - try { - data = await apiGet(enrichmentAnalyzersPath); - } - catch { - data = { status: "unavailable" }; - } - return { contents: [{ uri: "loopover://enrichment-analyzers", mimeType: "application/json", text: JSON.stringify(data, null, 2) }] }; -}); -server.registerResource("loopover_decision_pack", new ResourceTemplate("loopover://decision-packs/{login}", { list: undefined }), { - title: "LoopOver Decision Pack", - description: "Cached private contributor decision pack for a GitHub login. Requires authentication.", - mimeType: "application/json", -}, async (uri, { login }) => { - const payload = await getDecisionPackWithCache(String(login)); - return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(payload, null, 2) }] }; -}); -// ── Miner planning prompts (#293) ───────────────────────────────────────────── -server.registerPrompt("loopover_miner_select_issue", { - title: "Select Next Issue to Work On", - description: "Guide a contributor through selecting the best open issue to work on next, using LoopOver lane and duplicate signals. Advisory only — no GitHub writes.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - login: z.string().min(1).describe("GitHub login of the contributor."), - }, -}, ({ repoFullName, login }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver miner planning assistant for ${login} working on ${repoFullName}.`, - "", - "Your job is to help the contributor select the best open issue to work on next.", - "Use the loopover_get_repo_context and loopover_agent_plan_next_work tools to fetch lane and queue signals.", - "", - "Guidelines:", - "- Prefer issues that match the repo lane (feature, bug, docs, test, refactor, chore).", - "- Avoid issues with existing open PRs unless the contributor owns one of them.", - "- Flag duplicate or stale work before the contributor invests time.", - "- Summarize the top 3 candidate issues with a short rationale for each.", - "- Do not open, comment on, label, close, or modify any GitHub issue or PR.", - "- Do not predict reward amounts, payout estimates, or public scoreability rankings.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_miner_draft_pr_packet", { - title: "Draft PR Packet for Current Branch", - description: "Guide a contributor through preparing a public-safe PR packet for the current branch. Advisory only — no GitHub writes.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - login: z.string().min(1).describe("GitHub login of the contributor."), - }, -}, ({ repoFullName, login }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver miner planning assistant for ${login} working on ${repoFullName}.`, - "", - "Your job is to help the contributor prepare a public-safe PR packet for their current branch.", - "Use loopover_preflight_current_branch or loopover_prepare_pr_packet to gather branch signals.", - "", - "Guidelines:", - "- Draft a title, description, and label suggestions based on the diff metadata.", - "- Flag any preflight warnings (duplicate work, missing linked issue, test coverage gaps).", - "- Keep the draft public-safe: no private scoreability data, no raw trust scores.", - "- Present the draft for the contributor to review and edit before opening a PR.", - "- Do not open, comment on, label, close, or merge any GitHub PR.", - "- Do not predict reward amounts or publish scoring predictions.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_miner_branch_preflight", { - title: "Branch Preflight Check", - description: "Run a preflight check on the current branch and summarize blockers for the contributor. Advisory only.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - login: z.string().min(1).describe("GitHub login of the contributor."), - }, -}, ({ repoFullName, login }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver miner planning assistant for ${login} working on ${repoFullName}.`, - "", - "Your job is to run a branch preflight check and explain any blockers clearly.", - "Use loopover_explain_local_blockers and loopover_preflight_current_branch to fetch signals.", - "", - "Guidelines:", - "- List each blocker with a plain-language explanation and suggested remediation.", - "- Distinguish between hard blockers (will prevent merge) and soft warnings (worth fixing).", - "- Do not open, comment on, label, close, or merge any GitHub PR.", - "- Do not expose private scoreability details or raw trust scores in public-facing text.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_miner_cleanup_first", { - title: "Cleanup-First Planning", - description: "Help a contributor identify stale or low-value open PRs to close before opening new work. Advisory only.", - argsSchema: { - login: z.string().min(1).describe("GitHub login of the contributor."), - }, -}, ({ login }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver miner planning assistant for ${login}.`, - "", - "Your job is to help the contributor identify stale or low-value open PRs to close or supersede before opening new work.", - "Use loopover_get_decision_pack to fetch the contributor decision pack.", - "", - "Guidelines:", - "- List open PRs that are stale, duplicate, or conflicting with newer work.", - "- Suggest which to close, which to rebase, and which to keep open.", - "- Summarize the expected queue pressure impact of each decision.", - "- Do not close, comment on, label, or merge any GitHub PR autonomously.", - "- Do not predict reward amounts, payout estimates, or public scoring outcomes.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -// ── Maintainer and repo-owner workflow prompts (#294) ───────────────────────── -server.registerPrompt("loopover_maintainer_queue_triage", { - title: "Maintainer Queue Triage", - description: "Guide a maintainer through triaging the open PR queue using LoopOver signals. Advisory only — no GitHub writes.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - }, -}, ({ repoFullName }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver maintainer assistant for ${repoFullName}.`, - "", - "Your job is to help the maintainer triage the open PR queue.", - "Use loopover_get_repo_context to fetch current lane and queue signals.", - "", - "Guidelines:", - "- Group PRs by: ready to review, needs changes, stale, duplicate.", - "- Flag PRs with missing linked issues, failing checks, or low-quality diffs.", - "- Suggest a review order based on lane fit and contributor history.", - "- Prepare review notes and questions for the maintainer to post manually.", - "- Do not post comments, approve, request changes, label, close, or merge any PR autonomously.", - "- Do not expose private scoreability details, raw trust scores, or private reviewer context.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_maintainer_review_prep", { - title: "Maintainer Review Preparation", - description: "Prepare a structured review packet for a specific PR. Advisory only — no GitHub writes.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - pullNumber: z.string().min(1).describe("PR number to prepare a review for."), - }, -}, ({ repoFullName, pullNumber }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver maintainer assistant for ${repoFullName}.`, - "", - `Your job is to prepare a structured review packet for PR #${pullNumber}.`, - "Use loopover_preflight_pr or loopover_explain_repo_decision to fetch relevant signals.", - "", - "Guidelines:", - "- Summarize the PR scope, changed files, and linked issue (if any).", - "- List preflight signals: lane fit, duplicate risk, test coverage, queue pressure.", - "- Draft review questions or change requests for the maintainer to post manually.", - "- Keep all output public-safe: no private scoreability data or raw trust scores.", - "- Do not post review comments, approve, request changes, label, close, or merge the PR.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_maintainer_public_guidance", { - title: "Maintainer Public Guidance Draft", - description: "Draft low-noise, public-safe guidance for a contributor based on their PR. Advisory only — no GitHub writes.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - contributorLogin: z.string().min(1).describe("GitHub login of the contributor."), - }, -}, ({ repoFullName, contributorLogin }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver maintainer assistant for ${repoFullName}.`, - "", - `Your job is to draft low-noise, public-safe guidance for contributor ${contributorLogin}.`, - "Use loopover_get_repo_context for lane context.", - "", - "Guidelines:", - "- Draft a short, encouraging, actionable comment the maintainer can post manually.", - "- Focus on what the contributor should change, not on scoring or reward prediction.", - "- Keep the tone neutral and constructive — no compensation language.", - "- Do not mention trust scores, hotkeys, coldkeys, wallet addresses, reward estimates, or private reviewability.", - "- Do not post the comment autonomously — present it for the maintainer to review and post.", - "- Do not close, label, merge, or modify the PR autonomously.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_repo_owner_intake_readiness", { - title: "Repo Owner Intake Readiness", - description: "Guide a repo owner through assessing contributor intake readiness using LoopOver signals. Advisory only.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - }, -}, ({ repoFullName }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver repo-owner assistant for ${repoFullName}.`, - "", - "Your job is to help the repo owner assess contributor intake readiness.", - "Use loopover_get_repo_context to fetch lane and queue signals.", - "", - "Guidelines:", - "- Summarize current lane health: open issue count, PR queue pressure, merge rate.", - "- Flag gaps in the CONTRIBUTING.md, issue templates, or lane focus manifest.", - "- Recommend intake improvements the repo owner can make manually.", - "- Do not autonomously edit repo files, post comments, or open/close issues or PRs.", - "- Do not expose private scoreability data or raw trust scores publicly.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_repo_owner_focus_manifest_review", { - title: "Repo Owner Focus Manifest Review", - description: "Help a repo owner review and improve their focus manifest using LoopOver policy signals. Advisory only.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - }, -}, ({ repoFullName }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver repo-owner assistant for ${repoFullName}.`, - "", - "Your job is to help the repo owner review and improve their LoopOver focus manifest.", - "Use loopover_get_repo_context to fetch current policy and lane signals.", - "", - "Guidelines:", - "- Identify gaps or inconsistencies in the focus manifest.", - "- Suggest improvements to label policy, contribution lanes, and readiness criteria.", - "- Draft an updated manifest section for the repo owner to review and apply manually.", - "- Do not autonomously push changes to the repo or open PRs.", - "- Do not expose private scoreability data or raw trust scores.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -server.registerPrompt("loopover_repo_owner_onboarding_pack", { - title: "Repo Owner Onboarding Pack Planning", - description: "Help a repo owner plan and draft an onboarding pack for new contributors. Advisory only.", - argsSchema: { - repoFullName: z.string().min(3).describe("Target repository in owner/repo format."), - }, -}, ({ repoFullName }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: [ - `You are a LoopOver repo-owner assistant for ${repoFullName}.`, - "", - "Your job is to help the repo owner plan and draft an onboarding pack for new contributors.", - "Use loopover_get_repo_context to fetch lane and policy signals.", - "", - "Guidelines:", - "- Draft an onboarding overview: repo purpose, contribution lanes, good-first-issue guidance.", - "- Suggest CONTRIBUTING.md sections, issue templates, and label conventions to add or improve.", - "- Keep all content public-safe: no private scoreability, raw trust, or reward prediction.", - "- Present the draft for the repo owner to review and apply manually.", - "- Do not autonomously push changes, open PRs, or post comments.", - "- Do not request wallet, hotkey, coldkey, private keys, or tokens.", - ].join("\n"), - }, - }, - ], -})); -await server.connect(new StdioServerTransport()); -async function withClientWorkspaceRoots(input) { - return withWorkspaceRoots(input, await clientWorkspaceRoots()); -} -function withWorkspaceRoots(input, roots) { - return roots.length > 0 ? { ...input, workspaceRoots: roots } : input; -} -async function clientWorkspaceRoots() { - if (!server.server.getClientCapabilities()?.roots) - return []; - try { - const result = await server.server.listRoots(undefined, { timeout: 1000 }); - return Array.isArray(result.roots) ? result.roots : []; - } - catch { - return []; - } -} -function workspaceRootStatus(roots) { - const count = Array.isArray(roots) ? roots.length : 0; - return { - available: count > 0, - count, - pathsIncluded: false, - }; -} -function printMaintainHelp() { - process.stdout.write([ - "Usage: loopover-mcp maintain --repo owner/repo", - "", - "Maintainer controls for the agent auto-maintain layer (requires maintainer access; run `loopover-mcp login`).", - "", - "Subcommands:", - " status List the agent approval queue (auto_with_approval actions awaiting a decision).", - " queue List pending actions (id, kind, target) for approve/reject. Alias: pending.", - " propose Stage a new auto_with_approval action for a maintainer to approve later.", - ` classes: ${PROPOSE_ACTION_CLASSES.join(", ")}`, - " opts: --reason, --label, --review-body, --merge-method, --close-comment.", - " approve Approve a staged action -> execute it.", - " reject Reject a staged action -> cancel it.", - " pause Pause ALL agent actions on the repo (kill-switch).", - " resume Resume agent actions on the repo.", - " set-level Set the autonomy level for one action class.", - ` actions: ${MAINTAIN_ACTION_CLASSES.join(", ")}`, - ` levels: ${MAINTAIN_AUTONOMY_LEVELS.join(", ")}`, - " precision [--window-days N] Show gate false-positive telemetry (blocked-then-merged per gate type).", - " outcome-calibration Show slop-band merge rates and recommendation-outcome calibration.", - " [--window-days N] Bound the recommendation window (default: full history).", - " onboarding-pack [--refresh] Preview the repo's contributor onboarding pack.", - " audit-feed [--since ISO] Show the agent audit feed (who did what, when).", - " [--limit N] Cap the events returned (1-200).", - " [--pull N] Scope the feed to one pull request.", - " automation-state Show the derived agent automation state (mode, readiness, pending).", - " refresh-docs Open (or find the already-open) the AGENTS.md/CLAUDE.md generation PR.", - " generate-issue-drafts Preview contributor issue drafts (dry-run). Never creates without --create.", - " [--create] Actually open the drafted issues (requires repo write access).", - " [--limit N] Cap the drafts generated (1-20, default 5).", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -// #784 maintainer CLI controls — thin proxies over the agent approval-queue API (#779) and the maintainer -// settings kill-switch (#130). The API enforces maintainer authorization; the CLI never decides locally. -async function maintainCli(args) { - const subcommand = args[0]; - if (!subcommand || subcommand === "--help" || subcommand === "help") - return printMaintainHelp(); - const positional = args[1] && !args[1].startsWith("--") ? args[1] : undefined; - const options = parseOptions(args.slice(1)); - const repoFullName = options.repo; - if (!repoFullName || !repoFullName.includes("/")) - throw new Error("Pass --repo owner/repo."); - const [owner, repo] = repoFullName.split("/", 2); - const repoBase = `/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; - const queueBase = `${repoBase}/agent/pending-actions`; - const emit = (payload, line) => { - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`${line}\n`); - }; - if (subcommand === "status") { - const payload = await apiGet(queueBase); - const actions = payload.pendingActions ?? []; - // #6261: every field here is the API's. `emit` sends this string to the terminal only on the plain-text path - // (--json re-serializes `payload` instead), so sanitizing the composed line costs the JSON contract nothing. - emit(payload, [ - `Agent approval queue for ${repoFullName}: ${actions.length} pending.`, - ...actions.map((action) => `- ${sanitizePlainTextTerminalOutput(action.id)} ${sanitizePlainTextTerminalOutput(action.actionClass)} on #${sanitizePlainTextTerminalOutput(action.pullNumber)} ${sanitizePlainTextTerminalOutput(action.reason ?? "")}`), - ].join("\n")); - return; - } - // #2236 — explicit queue listing so maintainers can discover ids for approve/reject (alias: pending). - if (subcommand === "queue" || subcommand === "pending") { - const payload = await apiGet(queueBase); - const actions = payload.pendingActions ?? []; - emit(payload, [ - `Pending agent actions for ${repoFullName}: ${actions.length}.`, - ...actions.map((action) => { - // #6261: sanitize each field as it is read, so the fallback chains can't smuggle an escape in through - // whichever branch happens to win (`kind` alone has three sources). - const kind = sanitizePlainTextTerminalOutput(action.actionClass ?? action.kind ?? "unknown"); - const target = action.pullNumber != null ? `#${sanitizePlainTextTerminalOutput(action.pullNumber)}` : sanitizePlainTextTerminalOutput(action.target ?? "—"); - const summary = sanitizePlainTextTerminalOutput(action.reason ?? action.summary ?? ""); - return `- ${sanitizePlainTextTerminalOutput(action.id)} ${kind} ${target}${summary ? ` ${summary}` : ""}`; - }), - ].join("\n")); - return; - } - if (subcommand === "approve" || subcommand === "reject") { - if (!positional) - throw new Error(`Pass the pending-action id: loopover-mcp maintain ${subcommand} --repo owner/repo.`); - // The approval-queue route's decision verb is accept|reject (#779); the CLI exposes approve|reject. - const decision = subcommand === "approve" ? "accept" : "reject"; - const payload = await apiPost(`${queueBase}/${encodeURIComponent(positional)}/${decision}`, {}); - emit(payload, `${subcommand === "approve" ? "Accepted" : "Rejected"} ${positional}: ${payload.status ?? "ok"}${payload.executionOutcome ? ` (${payload.executionOutcome})` : ""}.`); - return; - } - if (subcommand === "propose") { - const actionClass = positional; - const pullArg = args[2] && !args[2].startsWith("--") ? args[2] : undefined; - if (!actionClass || !pullArg) { - throw new Error("Usage: loopover-mcp maintain propose --repo owner/repo [--reason ...] [--label ...] [--review-body ...] [--merge-method merge|squash|rebase] [--close-comment ...]."); - } - if (!PROPOSE_ACTION_CLASSES.includes(actionClass)) - throw new Error(`Unknown action class: ${actionClass}. Use ${PROPOSE_ACTION_CLASSES.join(", ")}.`); - const pullNumber = Number(pullArg); - if (!Number.isInteger(pullNumber) || pullNumber <= 0) - throw new Error(`Invalid pull number: ${pullArg}. Pass a positive integer.`); - const payload = await apiPost(queueBase, stripUndefined({ pullNumber, actionClass, reason: options.reason, label: options.label, reviewBody: options.reviewBody, mergeMethod: options.mergeMethod, closeComment: options.closeComment })); - const action = payload.action ?? {}; - emit(payload, `${payload.created ? "Staged" : "Already staged"} ${sanitizePlainTextTerminalOutput(action.actionClass ?? actionClass)} on ${repoFullName}#${pullNumber} (${sanitizePlainTextTerminalOutput(action.status ?? "pending")}), id ${sanitizePlainTextTerminalOutput(action.id ?? "?")}.`); - return; - } - if (subcommand === "pause" || subcommand === "resume") { - const payload = await apiFetch(`${repoBase}/settings`, { method: "PUT", body: JSON.stringify({ agentPaused: subcommand === "pause" }) }); - emit(payload, `Agent actions ${subcommand === "pause" ? "paused" : "resumed"} for ${repoFullName}.`); - return; - } - if (subcommand === "set-level") { - const action = args[1] && !args[1].startsWith("--") ? args[1] : undefined; - const level = args[2] && !args[2].startsWith("--") ? args[2] : undefined; - if (!action || !level) - throw new Error("Usage: loopover-mcp maintain set-level --repo owner/repo."); - if (!MAINTAIN_ACTION_CLASSES.includes(action)) - throw new Error(`Unknown action: ${action}. Use ${MAINTAIN_ACTION_CLASSES.join(", ")}.`); - if (!MAINTAIN_AUTONOMY_LEVELS.includes(level)) - throw new Error(`Unknown level: ${level}. Use ${MAINTAIN_AUTONOMY_LEVELS.join(", ")}.`); - // Read-merge-write so one class is updated without clearing the others. - const current = await apiGet(`${repoBase}/settings`); - const autonomy = { ...(current.autonomy ?? {}), [action]: level }; - const payload = await apiFetch(`${repoBase}/settings`, { method: "PUT", body: JSON.stringify({ autonomy }) }); - emit(payload, `Set ${action} autonomy to ${level} for ${repoFullName}.`); - return; - } - if (subcommand === "precision") { - // #554 gate false-positive telemetry: read-only measurement of blocked-then-merged PRs per gate type. - // The API enforces maintainer authorization; the CLI never decides locally. Optional --window-days bounds - // the block ledger the same way the route's ?windowDays query does (a non-positive value falls through to - // full history server-side). - const windowDays = Number(options.windowDays); - const query = windowDays > 0 ? `?windowDays=${encodeURIComponent(windowDays)}` : ""; - const payload = await apiGet(`${repoBase}/gate-precision${query}`); - const overall = payload.overall ?? {}; - const window = payload.windowDays ? `last ${payload.windowDays}d` : "all history"; - const rate = (value) => (value === null || value === undefined ? "n/a (below sample)" : `${Math.round(value * 100)}%`); - const lines = [ - `Gate precision for ${repoFullName} (${window}): ${overall.blocked ?? 0} blocked, ${overall.blockedThenMerged ?? 0} blocked-then-merged, false-positive rate ${rate(overall.falsePositiveRate)}.`, - ...(payload.perGateType ?? []).map((type) => `- ${type.gateType}: ${type.blocked} blocked, ${type.blockedThenMerged} merged anyway${type.falsePositiveRate === null ? "" : ` (${Math.round(type.falsePositiveRate * 100)}% FP)`}`), - ...(payload.signals ?? []), - ]; - emit(payload, lines.join("\n")); - return; - } - if (subcommand === "outcome-calibration") { - // #6735 outcome calibration: read-only measurement of whether higher-slop bands merge less often and how - // agent recommendations panned out. Same --window-days handling the sibling precision command uses (a - // non-positive value omits ?windowDays, so the server reports full history). - const windowDays = Number(options.windowDays); - const query = windowDays > 0 ? `?windowDays=${encodeURIComponent(windowDays)}` : ""; - const payload = await apiGet(`${repoBase}/outcome-calibration${query}`); - const window = payload.windowDays ? `last ${payload.windowDays}d` : "all history"; - const recommendations = payload.recommendations ?? {}; - const rate = (value) => (value === null || value === undefined ? "n/a (below sample)" : `${Math.round(value * 100)}%`); - const lines = [ - `Outcome calibration for ${repoFullName} (${window}): recommendations ${recommendations.positive ?? 0} positive, ${recommendations.negative ?? 0} negative, ${recommendations.pending ?? 0} pending (positive rate ${rate(recommendations.positiveRate)}).`, - ...(payload.slop ?? []).map((band) => `- ${band.band}: ${rate(band.mergeRate)} merge rate over ${band.sampleSize ?? 0} PR(s) (${band.merged ?? 0} merged, ${band.closed ?? 0} closed)`), - ...(payload.signals ?? []), - ]; - emit(payload, lines.join("\n")); - return; - } - if (subcommand === "onboarding-pack") { - // #6738: session-authenticated mirror of GET /onboarding-pack/preview (and the remote - // loopover_get_repo_onboarding_pack tool). Bare `--refresh` becomes options.refresh === true via - // parseOptions; omit the query otherwise so the default matches the precision-style GET pattern - // (server treats only the exact string "true" as a refresh). - const query = options.refresh === true ? "?refresh=true" : ""; - const payload = await apiGet(`${repoBase}/onboarding-pack/preview${query}`); - emit(payload, [ - `LoopOver onboarding pack preview for ${repoFullName} (preview-only, not published).`, - sanitizePlainTextTerminalOutput(JSON.stringify(payload.preview ?? payload, null, 2)), - ].join("\n")); - return; - } - if (subcommand === "audit-feed") { - // #6733: read-only mirror of GET {repoBase}/agent/audit-feed (the same surface the remote - // loopover_get_agent_audit_feed tool exposes). The API enforces maintainer authorization and validates - // every query param -- `since` must be ISO-8601, `limit` 1..200, `pull` a positive integer -- so the CLI - // forwards them verbatim rather than re-deciding locally, and a bad value surfaces as the API's own 400 - // detail. Omitted flags are omitted from the query entirely, so the route applies its own defaults. - const query = new URLSearchParams(); - if (options.since !== undefined) - query.set("since", String(options.since)); - if (options.limit !== undefined) - query.set("limit", String(options.limit)); - if (options.pull !== undefined) - query.set("pull", String(options.pull)); - const payload = await apiGet(`${repoBase}/agent/audit-feed${query.size > 0 ? `?${query}` : ""}`); - const events = payload.events ?? []; - // `pullNumber` is echoed by the route only on the ?pull= branch, so the scope line reports what was asked for. - const scope = payload.pullNumber ? `${repoFullName}#${payload.pullNumber}` : repoFullName; - emit(payload, [ - `Agent audit feed for ${scope}: ${events.length} event${events.length === 1 ? "" : "s"}.`, - // `detail` is the one free-form field here; sanitized on the plain-text path like onboarding-pack's - // dump above (--json re-serializes `payload` untouched, so the JSON contract is unaffected). - ...events.map((event) => sanitizePlainTextTerminalOutput([event.createdAt, event.eventType, event.actor, event.outcome, event.detail].filter(Boolean).join(" "))), - ].join("\n")); - return; - } - if (subcommand === "automation-state") { - // #6742: read-side counterpart to the write-side pause/resume/set-level above. Mirrors GET {repoBase}/ - // automation-state (and the loopover_get_automation_state MCP tool) — the DERIVED mode/permissionReadiness/ - // acting-classes/pending-count view the raw settings row omits. Read-only; the API enforces maintainer auth. - const payload = await apiGet(`${repoBase}/automation-state`); - const acting = payload.actingActionClasses ?? []; - emit(payload, [ - `Agent automation for ${repoFullName}: mode=${payload.mode}, ${acting.length} acting class(es), ${payload.pendingActionCount ?? 0} pending approval(s).`, - ` permission readiness: ${payload.permissionReadiness}`, - ` auto-maintain: ${payload.autoMaintain ?? "unset"}${payload.agentDryRun ? " (dry-run)" : ""}`, - acting.length > 0 ? ` acting classes: ${acting.join(", ")}` : " acting classes: none", - ].join("\n")); - return; - } - if (subcommand === "refresh-docs") { - // #6743: REST mirror of the loopover_refresh_repo_docs MCP tool -- only ever opens a PR (never merges, - // closes, or commits directly), so a single synchronous POST with no body is the whole contract. - const payload = await apiPost(`${repoBase}/repo-docs/refresh`, {}); - const line = payload.opened - ? `${payload.reused ? "Found the already-open" : "Opened a new"} repo-doc pull request for ${repoFullName}: ${sanitizePlainTextTerminalOutput(payload.url)}` - : `No repo-doc pull request opened for ${repoFullName}: ${sanitizePlainTextTerminalOutput(payload.reason)}`; - emit(payload, line); - return; - } - if (subcommand === "generate-issue-drafts") { - // #6757: session-authenticated mirror of POST {repoBase}/contributor-issue-drafts/generate (and the remote - // loopover_generate_contributor_issue_drafts tool). Dry-run BY DEFAULT — only a bare `--create` opts into - // the write path, and it is forwarded as {create:true, dryRun:false}, the exact shape the route's - // explicit_create_requires_dry_run_false guard demands. A plain `generate-issue-drafts` can never create. - const create = options.create === true; - const parsedLimit = Number(options.limit); - const body = { create, dryRun: !create, ...(Number.isFinite(parsedLimit) ? { limit: parsedLimit } : {}) }; - const payload = await apiPost(`${repoBase}/contributor-issue-drafts/generate`, body); - const mode = payload.dryRun ? "dry-run" : "create"; - const lines = [ - `Contributor issue drafts for ${repoFullName} (${mode}): ${payload.proposed ?? 0} proposed, ${payload.created ?? 0} created, ${payload.skippedDuplicate ?? 0} duplicate, ${payload.skippedDeclined ?? 0} declined, ${payload.skippedUnsafe ?? 0} unsafe, ${payload.skippedCreateFailed ?? 0} create-failed.`, - // draft.title/body are generated from untrusted repo issue data, so the plain-text path is sanitized (#6261). - ...(payload.drafts ?? []).map((draft) => { - const ref = draft.issue ? ` -> #${draft.issue.number} ${draft.issue.url}` : ""; - return `- [${sanitizePlainTextTerminalOutput(draft.status)}] ${sanitizePlainTextTerminalOutput(draft.title)}${sanitizePlainTextTerminalOutput(ref)}`; - }), - ]; - emit(payload, lines.join("\n")); - return; - } - throw new Error(`Unknown maintain subcommand: ${subcommand}. Use status | queue | propose | approve | reject | pause | resume | set-level | precision | outcome-calibration | onboarding-pack | audit-feed | automation-state | refresh-docs | generate-issue-drafts.`); -} -async function runCli(args) { - const command = args[0]; - if (command === "--help" || command === "help") - return printHelp(); - if (command === "--version" || command === "-v" || command === "version") - return printVersion(parseOptions(args.slice(1))); - if (command === "completion") - return completionCommand(args.slice(1)); - if (command === "tools") - return toolsCommand(args.slice(1)); - if (command === "agent") - return runAgentCli(args.slice(1)); - if (command === "cache") - return runCacheCli(args.slice(1)); - if (command === "maintain") - return maintainCli(args.slice(1)); - if (command === "telemetry") - return telemetryCommand(args.slice(1)); - const options = parseOptions(args.slice(1)); - if (command === "login") - return login(options); - if (command === "logout") - return logout(options); - if (command === "profile" || command === "profiles") - return profileCommand(args.slice(1)); - if (command === "whoami") - return whoami(options); - if (command === "config") - return configCommand(options); - if (command === "status") - return status(options); - if (command === "changelog") - return changelog(options); - if (command === "doctor") - return doctor(options); - if (command === "init-client") - return initClient(options); - if (command === "lint-pr-text") - return lintPrTextCli(args.slice(1)); - if (command === "validate-config") - return validateConfigCli(args.slice(1)); - if (command === "slop-risk") - return slopRiskCli(args.slice(1)); - if (command === "improvement-potential") - return improvementPotentialCli(args.slice(1)); - if (command === "issue-slop") - return issueSlopCli(args.slice(1)); - if (command === "decision-pack") - return decisionPackCli(options); - if (command === "repo-decision") - return repoDecisionCli(options); - if (command === "contributor-profile") - return contributorProfileCli(options); - if (command === "monitor-open-prs") - return monitorOpenPrsCli(options); - if (command === "pr-outcomes") - return prOutcomesCli(options); - if (command === "explain-review-risk") - return explainReviewRiskCli(options); - if (command === "notifications") - return notificationsCli(options); - if (command === "notifications-read") - return notificationsReadCli(options); - if (command === "watch") - return watchCli(args.slice(1)); - if (command === "review-pr") - return reviewPrCli(options); - if (command !== "analyze-branch" && command !== "preflight") { - const suggestion = suggestCommand(command); - throw new Error(`Unknown command: ${command}.${suggestion ? ` Did you mean \`${suggestion}\`?` : ""} Run \`loopover-mcp --help\` to list commands.`); - } - // Match every other subcommand: honor --help before requiring --login / hitting git+network (#6256). - if (options.help === true) - return printHelp(); - const contributorLogin = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!contributorLogin) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const result = await analyzeCurrentBranch({ - login: contributorLogin, - cwd: options.cwd, - repoFullName: options.repo, - baseRef: options.base, - title: options.title, - body: options.body, - labels: options.label, - linkedIssues: options.issue?.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0), - pendingMergedPrCount: optionalInteger(options.pendingMergedPrs), - pendingClosedPrCount: optionalInteger(options.pendingClosedPrs), - approvedPrCount: optionalInteger(options.approvedPrs), - expectedOpenPrCountAfterMerge: optionalInteger(options.expectedOpenPrs), - projectedCredibility: optionalNumber(options.projectedCredibility), - scenarioNotes: options.scenarioNote, - branchEligibility: branchEligibilityFromOptions(options), - validation: validationFromOptions(options), - scorePreviewCommand: options.scorePreviewCommand, - }); - const payload = command === "preflight" - ? { local: result.local, preflight: result.analysis.preflight, prPacket: result.analysis.prPacket, workspaceIntelligence: publicSafeWorkspaceIntelligence(result.analysis.workspaceIntelligence) } - : result; - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - if (options.format === "table") { - writeBranchAnalysisTable(result, command); - return; - } - writeBranchAnalysisCli(result, command); -} -// Render the report-shaped branch analysis (next actions, plus score blockers for analyze-branch) as -// aligned monospace tables when `--format table` is passed. Default and `--json` output are untouched. -function writeBranchAnalysisTable(result, command) { - const analysis = result.analysis; - const actionRows = (analysis.nextActions ?? []).map((action) => ({ - action: action.actionKind ?? "—", - priority: action.priorityScore === undefined || action.priorityScore === null ? "—" : String(action.priorityScore), - why: (action.whyThisHelps ?? []).join("; ") || "—", - })); - process.stdout.write(`${formatTable({ headers: [{ key: "action", label: "Action" }, { key: "priority", label: "Priority", align: "right" }, { key: "why", label: "Why this helps" }], rows: actionRows })}\n`); - if (command === "analyze-branch" && analysis.scoreBlockers?.length) { - process.stdout.write("\n"); - process.stdout.write(`${formatTable({ headers: [{ key: "blocker", label: "Score blocker" }], rows: analysis.scoreBlockers.map((blocker) => ({ blocker })) })}\n`); - } -} -function printReviewPrHelp() { - process.stdout.write([ - "Usage: loopover-mcp review-pr --login [--repo owner/repo] [--base origin/main] [--commit ]... [--body ] [--body-file ] [--linked-issue ] [--json]", - "", - "Compose the existing preflight + slop-risk + PR-text-lint checks into ONE pre-PR review report,", - "so a contributor's own local agent can see everything the loopover gate would flag before ever opening a PR.", - "Mirrors the loopover_review_pr_before_push MCP tool. Thin composition only — does not reimplement any check. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function reviewPrCli(options) { - if (options.help === true) - return printReviewPrHelp(); - const contributorLogin = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!contributorLogin) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - let prBody = options.body; - if (options.bodyFile) - prBody = readCliTextFile(options.bodyFile, "Body"); - const commitMessages = Array.isArray(options.commit) ? options.commit : options.commit ? [options.commit] : undefined; - const linkedIssue = parsePositiveIntegerOption(options.linkedIssue, "--linked-issue"); - const payload = await reviewLocalPr({ - login: contributorLogin, - cwd: options.cwd, - repoFullName: options.repo, - baseRef: options.base, - title: options.title, - body: prBody, - labels: options.label, - commitMessages, - linkedIssues: linkedIssue !== undefined ? [linkedIssue] : options.issue?.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`Pre-PR review: ${payload.overallStatus}\n`); - for (const section of payload.sections) - process.stdout.write(`- ${section.name}: ${section.status}\n`); - process.stdout.write(`Preflight: ${payload.preflight.status}\n`); - if (payload.slopRisk) - process.stdout.write(`Slop risk: ${payload.slopRisk.band}\n`); - else if (payload.slopRiskError) - process.stdout.write(`Slop risk: unavailable (${payload.slopRiskError})\n`); - if (payload.prTextLint) - process.stdout.write(`PR text lint: ${payload.prTextLint.verdict} (score ${payload.prTextLint.score})\n`); - else if (payload.prTextLintError) - process.stdout.write(`PR text lint: unavailable (${payload.prTextLintError})\n`); -} -// Opens, type-checks, and reads the file through ONE file descriptor rather than a separate -// stat-then-read pair: a check-then-read on a path string leaves a race window where a symlink or -// special file (FIFO, device) can be swapped in between the two calls, letting the earlier -// isFile()/size validation apply to a different, unvalidated file than the one actually read. -// O_NOFOLLOW makes a symlinked path fail to open outright instead of silently following it. -function readCliTextFile(path, label) { - let fd; - try { - fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW); - } - catch (error) { - if (error && error.code === "ENOENT") - throw new Error(`${label} file not found: ${path}`); - if (error && (error.code === "ELOOP" || error.code === "EMLINK")) - throw new Error(`${label} file must be a regular file: ${path}`); - throw error; - } - try { - const stats = fstatSync(fd); - if (!stats.isFile()) - throw new Error(`${label} file must be a regular file: ${path}`); - if (stats.size > cliTextFileMaxBytes) - throw new Error(`${label} file is too large: ${path} (max ${cliTextFileMaxBytes} bytes)`); - // Bound the READ itself rather than trusting stats.size alone: a regular file can grow between fstatSync - // and the read below (the fd is the same, but nothing stops another process from appending to the file - // in between), so read at most cliTextFileMaxBytes + 1 bytes directly from the descriptor and fail if that - // cap is exceeded, instead of handing the now-possibly-stale size to an unbounded readFileSync. - const buffer = Buffer.alloc(cliTextFileMaxBytes + 1); - let bytesRead = 0; - while (bytesRead < buffer.length) { - const n = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null); - if (n === 0) - break; - bytesRead += n; - } - if (bytesRead > cliTextFileMaxBytes) - throw new Error(`${label} file is too large: ${path} (max ${cliTextFileMaxBytes} bytes)`); - return buffer.subarray(0, bytesRead).toString("utf8"); - } - finally { - closeSync(fd); - } -} -function printLintPrTextHelp() { - process.stdout.write([ - "Usage: loopover-mcp lint-pr-text [--commit ]... [--body ] [--body-file ] [--linked-issue ] [--json]", - "", - "Lint a commit message and PR body against the LoopOver traceability and Conventional Commit rubric.", - "Mirrors the loopover_lint_pr_text MCP tool and POST /v1/lint/pr-text. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function lintPrTextCli(args) { - if (!args.length || args[0] === "--help" || args[0] === "help") - return printLintPrTextHelp(); - const options = parseOptions(args); - const commitMessages = Array.isArray(options.commit) ? options.commit : options.commit ? [options.commit] : undefined; - let prBody = options.body; - if (options.bodyFile) { - prBody = readCliTextFile(options.bodyFile, "Body"); - } - const linkedIssue = parsePositiveIntegerOption(options.linkedIssue, "--linked-issue"); - const payload = await apiPost("/v1/lint/pr-text", { - ...(commitMessages?.length ? { commitMessages } : {}), - ...(prBody !== undefined ? { prBody } : {}), - ...(linkedIssue !== undefined ? { linkedIssue } : {}), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`PR text lint: ${payload.verdict} (score ${payload.score})\n`); - process.stdout.write(`${payload.summary}\n`); - for (const fix of payload.fixes ?? []) - process.stdout.write(`- ${fix}\n`); -} -// Strip ANSI escapes + control characters from text this CLI prints as plain text. Rule (#6261): every value that -// reaches a terminal from a source the user does not control -- an API response, or free text the API echoed back -// from a third-party issue/PR -- goes through this first. Otherwise a hostile string can repaint the screen, -// rewrite earlier lines, or fake a success next to a real failure, since the terminal cannot tell our text from -// the payload's. -// -// Two things deliberately do NOT go through it: -// - `--json` output. JSON.stringify escapes U+001B (and the rest of U+0000-U+001F) as a \u001b literal, so an escape -// sequence cannot survive into the printed document -- and sanitizing there would corrupt the machine-readable -// contract callers parse. -// - Our own literals, and values the user themself passed in (--login, --repo). Those are already the user's, -// and the CLI prints no colour of its own -- there is no intentional ANSI in this file to preserve. -function sanitizePlainTextTerminalOutput(value) { - return String(value) - .replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[PX^_][^\x1b]*(?:\x1b\\)|[@-_])/g, "") - .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, ""); -} -function printValidateConfigHelp() { - process.stdout.write([ - "Usage: loopover-mcp validate-config --file [--source repo_file|api_record|none] [--json]", - "", - "Validate a .loopover.yml manifest before pushing.", - "Mirrors the loopover_validate_config MCP tool and POST /v1/validate/focus-manifest. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function validateConfigCli(args) { - if (!args.length || args[0] === "--help" || args[0] === "help") - return printValidateConfigHelp(); - const options = parseOptions(args); - if (!options.file) - throw new Error("Pass --file to the manifest to validate."); - const content = readCliTextFile(options.file, "Manifest"); - const source = options.source; - if (source !== undefined && !["repo_file", "api_record", "none"].includes(String(source))) { - throw new Error("--source must be one of: repo_file, api_record, none"); - } - const payload = await apiPost("/v1/validate/focus-manifest", { - content, - ...(source !== undefined ? { source } : {}), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`Manifest validation: ${payload.status}\n`); - process.stdout.write(`present=${payload.present}\n`); - for (const warning of payload.warnings ?? []) - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(warning)}\n`); -} -function printSlopRiskHelp() { - process.stdout.write([ - "Usage: loopover-mcp slop-risk [--description ] [--description-file ] [--changed-file ]... [--test ]... [--test-file ]... [--json]", - "", - "Assess deterministic slop risk from local diff metadata and a PR description.", - "Mirrors the loopover_check_slop_risk MCP tool and POST /v1/lint/slop-risk. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -function stringArrayOption(value) { - if (!value) - return []; - return Array.isArray(value) ? value : [value]; -} -function parseChangedFileSpec(raw) { - const [path, additions, deletions] = String(raw).split(":"); - if (!path) - throw new Error(`Invalid --changed-file value: ${raw}`); - const entry = { path }; - if (additions !== undefined && additions !== "") { - const parsedAdditions = Number(additions); - if (!Number.isInteger(parsedAdditions) || parsedAdditions < 0) - throw new Error(`Invalid additions in --changed-file: ${raw}`); - entry.additions = parsedAdditions; - } - if (deletions !== undefined && deletions !== "") { - const parsedDeletions = Number(deletions); - if (!Number.isInteger(parsedDeletions) || parsedDeletions < 0) - throw new Error(`Invalid deletions in --changed-file: ${raw}`); - entry.deletions = parsedDeletions; - } - return entry; -} -async function slopRiskCli(args) { - if (!args.length || args[0] === "--help" || args[0] === "help") - return printSlopRiskHelp(); - const options = parseOptions(args); - let description = options.description ?? options.body; - const descriptionFile = options.descriptionFile ?? options.bodyFile; - if (descriptionFile) { - description = readCliTextFile(descriptionFile, "Description"); - } - const changedFiles = stringArrayOption(options.changedFile).map(parseChangedFileSpec); - const tests = stringArrayOption(options.test); - const testFiles = stringArrayOption(options.testFile); - const payload = await apiPost("/v1/lint/slop-risk", { - ...(changedFiles.length ? { changedFiles } : {}), - ...(description !== undefined ? { description } : {}), - ...(tests.length ? { tests } : {}), - ...(testFiles.length ? { testFiles } : {}), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - // #6990: the route now returns band + findings only (no numeric score/rubric), matching the MCP tool's - // blunting; print the band alone so the CLI can't leak the exact score the REST surface no longer sends. - process.stdout.write(`Slop risk: ${sanitizePlainTextTerminalOutput(payload.band)}\n`); - for (const finding of payload.findings ?? []) - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(finding.title)}: ${sanitizePlainTextTerminalOutput(finding.detail)}\n`); -} -function printImprovementPotentialHelp() { - process.stdout.write([ - "Usage: loopover-mcp improvement-potential [--changed-file ]... [--test ]... [--test-file ]... [--patch-coverage-delta ] [--json]", - "", - "Assess deterministic structural-improvement potential from local diff metadata.", - "Mirrors the loopover_check_improvement_potential MCP tool and POST /v1/lint/improvement-potential. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function improvementPotentialCli(args) { - // #6748: shell CLI mirror of loopover_check_improvement_potential, matching slopRiskCli's HTTP-proxy pattern - // (the pure builder lives in src/signals/improvement.ts, not yet an @loopover/engine export for in-process use). - if (!args.length || args[0] === "--help" || args[0] === "help") - return printImprovementPotentialHelp(); - const options = parseOptions(args); - const changedFiles = stringArrayOption(options.changedFile).map(parseChangedFileSpec); - const tests = stringArrayOption(options.test); - const testFiles = stringArrayOption(options.testFile); - let patchCoverageDeltaPercent; - if (options.patchCoverageDelta !== undefined) { - patchCoverageDeltaPercent = Number(options.patchCoverageDelta); - if (!Number.isFinite(patchCoverageDeltaPercent)) { - throw new Error("--patch-coverage-delta must be a finite number"); - } - } - const payload = await apiPost("/v1/lint/improvement-potential", { - ...(changedFiles.length ? { changedFiles } : {}), - ...(tests.length ? { tests } : {}), - ...(testFiles.length ? { testFiles } : {}), - ...(patchCoverageDeltaPercent !== undefined ? { patchCoverageDeltaPercent } : {}), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`Improvement potential: ${sanitizePlainTextTerminalOutput(payload.improvementScore)} (${sanitizePlainTextTerminalOutput(payload.band)})\n`); - for (const finding of payload.findings ?? []) - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(finding.title)}: ${sanitizePlainTextTerminalOutput(finding.detail)}\n`); -} -function printIssueSlopHelp() { - process.stdout.write([ - "Usage: loopover-mcp issue-slop [--title ] [--body ] [--body-file ] [--json]", - "", - "Assess deterministic issue slop risk from an issue title and body alone.", - "Mirrors the loopover_check_issue_slop MCP tool and POST /v1/lint/issue-slop. Advisory only; no source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function issueSlopCli(args) { - if (!args.length || args[0] === "--help" || args[0] === "help") - return printIssueSlopHelp(); - const options = parseOptions(args); - let body = normalizeOptionalStringOption(options.body); - if (options.bodyFile) { - body = readCliTextFile(options.bodyFile, "Body"); - } - const title = normalizeOptionalStringOption(options.title); - const payload = await apiPost("/v1/lint/issue-slop", { - ...(title !== undefined ? { title } : {}), - ...(body !== undefined ? { body } : {}), - }); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - // #6990: band + findings only, matching the route's blunting (no numeric score/rubric leaked through the CLI). - process.stdout.write(`Issue slop risk: ${sanitizePlainTextTerminalOutput(payload.band)}\n`); - for (const finding of payload.findings ?? []) - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(finding.title)}: ${sanitizePlainTextTerminalOutput(finding.detail)}\n`); -} -function printDecisionPackHelp() { - process.stdout.write([ - "Usage: loopover-mcp decision-pack --login [--json]", - "", - "Fetch the cached (or freshly built) contributor decision pack for a GitHub login.", - "Mirrors the loopover_get_decision_pack MCP tool and GET /v1/contributors/{login}/decision-pack. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -function printContributorProfileHelp() { - process.stdout.write([ - "Usage: loopover-mcp contributor-profile --login [--json]", - "", - "Fetch the contributor profile for a GitHub login.", - "Mirrors the loopover_get_contributor_profile MCP tool and GET /v1/contributors/{login}/profile. No source upload.", - "", - "Login resolves from --login, the active session, LOOPOVER_LOGIN, then GITHUB_LOGIN.", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -// #6737: CLI mirror of the loopover_get_contributor_profile MCP tool and GET /v1/contributors/{login}/profile -// (requireContributorAccess-gated -- the same gate decision-pack/repo-decision already satisfy). Login resolves -// from --login / the active session / LOOPOVER_LOGIN / GITHUB_LOGIN, exactly like the sibling contributor -// commands, so an already-logged-in contributor never retypes their own login. Named `contributor-profile` -// because the top-level `profile` command already manages MCP client profiles. -async function contributorProfileCli(options) { - if (options.help === true) - return printContributorProfileHelp(); - const login = options.login ?? activeProfile.session?.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login , log in with `loopover-mcp login`, or set LOOPOVER_LOGIN."); - const payload = await apiGet(`/v1/contributors/${encodeURIComponent(login)}/profile`); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)} -`); - return; - } - process.stdout.write(`LoopOver contributor profile for ${login}. -`); - if (payload.summary) - process.stdout.write(`${sanitizePlainTextTerminalOutput(payload.summary)} -`); -} -async function decisionPackCli(options) { - if (options.help === true) - return printDecisionPackHelp(); - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const payload = await getDecisionPackWithCache(login); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - // #6261: decisionPackToolSummary is left alone -- verified, not assumed. It interpolates `login` (the user's own - // --login/env value) and `payload.freshness`, and freshness only ever reaches the string inside an equality guard - // against the literals "stale"/"rebuilding", so the API cannot route text of its own choosing through it. - process.stdout.write(`${decisionPackToolSummary(login, payload)}\n`); - if (payload.summary) - process.stdout.write(`${sanitizePlainTextTerminalOutput(payload.summary)}\n`); - if (payload.cache?.rerunGuidance) - process.stdout.write(`Rerun when: ${sanitizePlainTextTerminalOutput(payload.cache.rerunGuidance)}\n`); -} -function printMonitorOpenPrsHelp() { - process.stdout.write([ - "Usage: loopover-mcp monitor-open-prs --login [--json]", - "", - "Review your open PRs across registered repos: queue classification and next steps per PR.", - "Mirrors the loopover_monitor_open_prs MCP tool and GET /v1/contributors/{login}/open-pr-monitor. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function monitorOpenPrsCli(options) { - if (options.help === true) - return printMonitorOpenPrsHelp(); - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const payload = await getOpenPrMonitor(login); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - // #6261: every value below is the API's to choose -- the summary/guidance it composes, and PR titles it echoes - // back from third-party repos -- so all of it is sanitized before it reaches the terminal. `login` is the user's - // own --login/env value and needs no sanitizing, but it only reaches stdout via the literal fallback branch. - process.stdout.write(`${sanitizePlainTextTerminalOutput(openPrMonitorToolSummary(login, payload))}\n`); - for (const line of payload?.guidance ?? []) - process.stdout.write(`${sanitizePlainTextTerminalOutput(line)}\n`); - for (const pr of payload?.pullRequests ?? []) { - const heading = `${pr.repoFullName}#${pr.number} [${pr.classification}] ${pr.title}`; - process.stdout.write(`${sanitizePlainTextTerminalOutput(heading)}\n`); - for (const step of pr.nextSteps ?? []) - process.stdout.write(` - ${sanitizePlainTextTerminalOutput(step)}\n`); - } -} -function printPrOutcomesHelp() { - process.stdout.write([ - "Usage: loopover-mcp pr-outcomes --login [--limit N] [--json]", - "", - "List your post-merge PR outcome history (public-safe attribution per merged PR).", - "Mirrors the loopover_pr_outcome MCP tool and GET /v1/contributors/{login}/pr-outcomes. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function prOutcomesCli(options) { - if (options.help === true) - return printPrOutcomesHelp(); - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const limitRaw = options.limit; - let limit; - if (limitRaw !== undefined && limitRaw !== true) { - const parsed = Number(limitRaw); - if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) { - throw new Error("Pass --limit as an integer between 1 and 100."); - } - limit = parsed; - } - const payload = await getPrOutcomes(login, limit); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`${sanitizePlainTextTerminalOutput(prOutcomesToolSummary(login, payload))}\n`); - for (const outcome of payload?.outcomes ?? []) { - const heading = `${outcome.repoFullName}#${outcome.pullNumber ?? "?"} [${outcome.outcome}]`; - process.stdout.write(`${sanitizePlainTextTerminalOutput(heading)}\n`); - if (outcome.attribution) - process.stdout.write(` ${sanitizePlainTextTerminalOutput(outcome.attribution)}\n`); - } -} -function printExplainReviewRiskHelp() { - process.stdout.write([ - "Usage: loopover-mcp explain-review-risk --repo owner/repo --title [--login ] [--body ] [--json]", - "", - "Explain review risk for a planned PR (preflight + optional role context + recommendation).", - "Mirrors the loopover_explain_review_risk MCP tool and POST /v1/preflight/review-risk. No source upload.", - "", - "Pass --repo or --repoFullName, --title, and optionally --login as contributorLogin.", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function explainReviewRiskCli(options) { - if (options.help === true) - return printExplainReviewRiskHelp(); - const repoFullName = options.repoFullName ?? options.repo; - if (!repoFullName || !String(repoFullName).includes("/")) - throw new Error("Pass --repo owner/repo or --repoFullName owner/repo."); - if (!options.title) - throw new Error("Pass --title ."); - const contributorLogin = options.login ?? options.contributorLogin; - const labels = Array.isArray(options.label) ? options.label : options.label ? [options.label] : undefined; - const changedFiles = Array.isArray(options.changedFile) ? options.changedFile : options.changedFile ? [options.changedFile] : undefined; - const linkedIssues = Array.isArray(options.issue) - ? options.issue.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0) - : options.issue - ? [Number(options.issue)].filter((value) => Number.isInteger(value) && value > 0) - : undefined; - const tests = Array.isArray(options.test) ? options.test : options.test ? [options.test] : undefined; - const payload = await apiPost("/v1/preflight/review-risk", stripUndefined({ - repoFullName, - title: options.title, - contributorLogin, - body: options.body, - labels, - changedFiles, - linkedIssues: linkedIssues && linkedIssues.length > 0 ? linkedIssues : undefined, - tests, - authorAssociation: options.authorAssociation, - })); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`${sanitizePlainTextTerminalOutput(payload.summary ?? `LoopOver review-risk explanation for ${repoFullName}.`)}\n`); - if (payload.recommendation) - process.stdout.write(`Recommendation: ${sanitizePlainTextTerminalOutput(payload.recommendation)}\n`); - if (payload.preflight?.status) - process.stdout.write(`Preflight status: ${sanitizePlainTextTerminalOutput(payload.preflight.status)}\n`); -} -function printNotificationsHelp() { - process.stdout.write([ - "Usage: loopover-mcp notifications --login [--json]", - "", - "Your own badge notification feed (newest first) with an unread count, self-scoped.", - "Mirrors the loopover_list_notifications MCP tool and GET /v1/contributors/{login}/notifications. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -// #6745: CLI mirror of loopover_list_notifications. Login resolves from --login / the active session / -// LOOPOVER_LOGIN / GITHUB_LOGIN, like the sibling contributor commands. -async function notificationsCli(options) { - if (options.help === true) - return printNotificationsHelp(); - const login = options.login ?? activeProfile.session?.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login , log in with `loopover-mcp login`, or set LOOPOVER_LOGIN."); - const payload = await getNotifications(login); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`LoopOver notifications for ${login}: ${payload.unreadCount} unread.\n`); - for (const item of payload.notifications ?? []) { - // `login` is the user's own value; the API chooses the title text, so it is sanitized before the terminal. - const flag = item.status === "delivered" ? "*" : " "; - process.stdout.write(`${sanitizePlainTextTerminalOutput(`${flag} ${item.repoFullName}#${item.pullNumber} ${item.title}`)}\n`); - } -} -// #6746: contributor-scoped mirror of the loopover_watch_issues MCP tool and the /v1/contributors/{login}/watches -// route family. The MCP tool's action enum maps to subcommands here: list=GET, add=POST, remove=DELETE. -async function watchCli(args) { - const subcommand = args[0]; - if (!subcommand || subcommand === "--help" || subcommand === "help") - return printWatchHelp(); - const positional = args[1] && !args[1].startsWith("--") ? args[1] : undefined; - const options = parseOptions(args.slice(1)); - const login = options.login ?? activeProfile.session?.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login , log in with `loopover-mcp login`, or set LOOPOVER_LOGIN."); - const base = `/v1/contributors/${encodeURIComponent(login)}/watches`; - // The API chooses `changed` / repo / label text, so the plain-text path is sanitized (#6261); `login` is the - // user's own value. - const render = (payload) => [ - `Watching ${(payload.watching ?? []).length} repo(s) for ${login}${payload.changed ? ` (${sanitizePlainTextTerminalOutput(payload.changed)})` : ""}.`, - ...(payload.watching ?? []).map((watch) => { - const labels = (watch.labels ?? []).length > 0 ? ` [${watch.labels.map(sanitizePlainTextTerminalOutput).join(", ")}]` : ""; - return `- ${sanitizePlainTextTerminalOutput(watch.repoFullName)}${labels}`; - }), - ].join("\n"); - const emit = (payload) => { - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`${render(payload)}\n`); - }; - if (subcommand === "list") { - emit(await apiGet(base)); - return; - } - if (subcommand === "add" || subcommand === "remove") { - if (!positional || !positional.includes("/")) { - throw new Error(`Pass the repo: loopover-mcp watch ${subcommand} .`); - } - if (subcommand === "add") { - const labels = typeof options.labels === "string" ? options.labels.split(",").map((label) => label.trim()).filter(Boolean) : []; - emit(await apiPost(base, { repoFullName: positional, ...(labels.length > 0 ? { labels } : {}) })); - } - else { - emit(await apiDelete(base, { repoFullName: positional })); - } - return; - } - throw new Error(`Unknown watch subcommand: ${subcommand}. Use list | add [--labels a,b] | remove .`); -} -function printWatchHelp() { - process.stdout.write([ - "Usage: loopover-mcp watch [owner/repo] [--labels a,b] [--login ] [--json]", - "", - "Manage your issue-watch subscriptions (mirrors the loopover_watch_issues MCP tool and the", - "/v1/contributors/{login}/watches routes):", - " list Show the repos you are watching.", - " add [--labels] Watch a repo for new grabbable issues (optional comma-separated label filter).", - " remove Stop watching a repo.", - "", - "Login resolves from --login, the active session, LOOPOVER_LOGIN, then GITHUB_LOGIN.", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -function printNotificationsReadHelp() { - process.stdout.write([ - "Usage: loopover-mcp notifications-read --login [--id ]... [--json]", - "", - "Mark your delivered notifications read. With no --id, marks all of them.", - "Mirrors the loopover_mark_notifications_read MCP tool and POST /v1/contributors/{login}/notifications/read.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -// #6745: CLI mirror of loopover_mark_notifications_read. Repeated --id flags collect into an ids array; omitting -// them marks every delivered notification read (mirrors the route's absent-body behavior). -async function notificationsReadCli(options) { - if (options.help === true) - return printNotificationsReadHelp(); - const login = options.login ?? activeProfile.session?.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login , log in with `loopover-mcp login`, or set LOOPOVER_LOGIN."); - const ids = Array.isArray(options.id) ? options.id : options.id ? [options.id] : undefined; - const payload = await postMarkNotificationsRead(login, ids); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`Marked ${payload.marked} LoopOver notification(s) read for ${login}.\n`); -} -function printRepoDecisionHelp() { - process.stdout.write([ - "Usage: loopover-mcp repo-decision --login --repo owner/repo [--json]", - "", - "Fetch the cached (or freshly built) repo decision for a GitHub login and repo.", - "Mirrors the loopover_explain_repo_decision MCP tool. No source upload.", - "", - "Pass --json for machine-readable output.", - ].join("\n") + "\n"); -} -async function repoDecisionCli(options) { - if (options.help === true) - return printRepoDecisionHelp(); - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const repoFullName = options.repo; - if (!repoFullName || !repoFullName.includes("/")) - throw new Error("Pass --repo owner/repo."); - const [owner, repo] = repoFullName.split("/", 2); - const payload = await getRepoDecisionWithCache(login, owner, repo); - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - // #6261: repoDecisionToolSummary is left alone for the same reason -- it interpolates only `login` and - // `repoFullName`, both of which the user typed on their own command line. No payload text reaches it. - process.stdout.write(`${repoDecisionToolSummary(login, repoFullName, payload)}\n`); - const actions = payload.decision?.nextActions ?? payload.decision?.publicNextActions ?? []; - for (const action of actions.slice(0, 3)) - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(action)}\n`); - if (payload.cache?.rerunGuidance) - process.stdout.write(`Rerun when: ${sanitizePlainTextTerminalOutput(payload.cache.rerunGuidance)}\n`); -} -function runCacheCli(args) { - const subcommand = args[0] ?? "help"; - if (subcommand === "--help" || subcommand === "help") - return printCacheHelp(); - const options = parseOptions(args.slice(1)); - if (subcommand === "clear") { - const payload = clearDecisionPackCache(); - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Cleared ${payload.removed} decision-pack cache entr${payload.removed === 1 ? "y" : "ies"}.\n`); - return; - } - if (subcommand === "status") { - const payload = inspectDecisionPackCache(); - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Decision-pack cache: ${payload.entries} entr${payload.entries === 1 ? "y" : "ies"}.\n`); - return; - } - if (subcommand === "list" || subcommand === "ls") { - const payload = listDecisionPackCache(); - if (emitList(options, payload.entries, payload)) - return; - if (payload.count === 0) - process.stdout.write("Decision-pack cache is empty.\n"); - else - for (const entry of payload.entries) - process.stdout.write(`- ${entry.login ?? "unknown"} (cached ${entry.cachedAt ?? "unknown"}, ${entry.bytes} bytes)\n`); - return; - } - throw new Error(`Unknown cache command: ${subcommand}`); -} -async function runAgentCli(args) { - const subcommand = args[0] ?? "help"; - if (subcommand === "--help" || subcommand === "help") - return printAgentHelp(); - const options = parseOptions(args.slice(1)); - if (subcommand === "plan") { - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const payload = await apiPost("/v1/agent/plan-next-work", stripUndefined({ login, repoFullName: options.repo, objective: options.objective, surface: "mcp" })); - return outputAgentPayload(payload, options, `LoopOver agent plan: ${payload.summary ?? payload.run?.status ?? "ready"}`); - } - if (subcommand === "status") { - const runId = args[1] && !args[1].startsWith("--") ? args[1] : options.runId; - if (!runId) - throw new Error("Usage: loopover-mcp agent status "); - const payload = await apiGet(`/v1/agent/runs/${encodeURIComponent(runId)}`); - return outputAgentPayload(payload, options, `LoopOver agent run ${runId}: ${payload.run?.status ?? "unknown"}`); - } - if (subcommand === "explain") { - const runId = args[1] && !args[1].startsWith("--") ? args[1] : options.runId; - if (!runId) - throw new Error("Usage: loopover-mcp agent explain "); - const payload = await apiGet(`/v1/agent/runs/${encodeURIComponent(runId)}`); - const topAction = payload.actions?.[0] ?? null; - return outputAgentPayload({ ...payload, topAction }, options, topAction ? `Top action: ${topAction.recommendation}` : "No top action is available yet."); - } - if (subcommand === "packet") { - const login = options.login ?? process.env.LOOPOVER_LOGIN ?? process.env.GITHUB_LOGIN; - if (!login) - throw new Error("Pass --login or set LOOPOVER_LOGIN."); - const payload = await agentPreparePrPacket({ - login, - cwd: options.cwd, - repoFullName: options.repo, - baseRef: options.base, - title: options.title, - body: options.body, - labels: options.label, - linkedIssues: options.issue?.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0), - pendingMergedPrCount: optionalInteger(options.pendingMergedPrs), - pendingClosedPrCount: optionalInteger(options.pendingClosedPrs), - approvedPrCount: optionalInteger(options.approvedPrs), - expectedOpenPrCountAfterMerge: optionalInteger(options.expectedOpenPrs), - projectedCredibility: optionalNumber(options.projectedCredibility), - scenarioNotes: options.scenarioNote, - branchEligibility: branchEligibilityFromOptions(options), - validation: validationFromOptions(options), - scorePreviewCommand: options.scorePreviewCommand, - }); - return outputAgentPayload(payload, options, "LoopOver public-safe PR packet prepared."); - } - throw new Error(`Unknown agent command: ${subcommand}`); -} -function outputAgentPayload(payload, options, summary) { - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - const packetMarkdown = payload?.prPacket?.markdown ?? payload?.actions?.find((action) => action?.actionType === "prepare_pr_packet")?.payload?.prPacket?.markdown; - if (typeof packetMarkdown === "string" && packetMarkdown.trim()) { - const safeMarkdown = requirePublicSafePacketMarkdown(packetMarkdown); - return process.stdout.write(safeMarkdown.endsWith("\n") ? safeMarkdown : `${safeMarkdown}\n`); - } - process.stdout.write(`${summary}\n`); - if (payload.summary && payload.summary !== summary) - process.stdout.write(`${payload.summary}\n`); - if (payload.recommendedRerunCondition) - process.stdout.write(`Rerun when: ${payload.recommendedRerunCondition}\n`); - const actions = payload.actions ?? payload.nextActions ?? []; - for (const action of actions.slice(0, 3)) { - const label = action.actionType ?? action.actionKind ?? action.recommendation ?? "action"; - const detail = action.recommendation ?? action.actionKind ?? action.summary ?? label; - process.stdout.write(`- ${label}: ${detail}\n`); - if (action.explanationCard) { - process.stdout.write(` why now: ${action.explanationCard.whyNow}\n`); - process.stdout.write(` impact: ${action.explanationCard.expectedImpact}\n`); - process.stdout.write(` rerun: ${action.explanationCard.rerunWhen}\n`); - } - else if (action.rerunWhen) { - process.stdout.write(` rerun: ${action.rerunWhen}\n`); - } - } -} -function writeBranchAnalysisCli(result, command) { - const analysis = result.analysis; - const intelligence = command === "preflight" ? publicSafeWorkspaceIntelligence(analysis.workspaceIntelligence) : analysis.workspaceIntelligence; - process.stdout.write(`${analysis.summary}\n`); - process.stdout.write(`Top action: ${analysis.nextActions?.[0]?.actionKind ?? "none"}\n`); - if (analysis.nextActions?.[0]?.whyThisHelps?.length) { - process.stdout.write("Why this helps:\n"); - for (const line of analysis.nextActions[0].whyThisHelps.slice(0, 3)) - process.stdout.write(`- ${line}\n`); - } - if (intelligence) - writeWorkspaceIntelligenceCli(intelligence); - if (command === "analyze-branch" && analysis.scoreBlockers?.length) { - process.stdout.write("Score blockers:\n"); - for (const blocker of analysis.scoreBlockers.slice(0, 5)) - process.stdout.write(`- ${blocker}\n`); - } - process.stdout.write(`Preflight: ${analysis.preflight.status}\n`); - process.stdout.write(`Source upload: disabled\n`); - if (result.local?.localScorerStatus?.ok === false) { - process.stdout.write(`Local scorer: ${result.local.localScorerStatus.code ?? "metadata_only"}\n`); - for (const line of result.local.setupGuidance ?? setupGuidanceForLocalScorer(result.local.localScorerStatus)) { - process.stdout.write(`- ${line}\n`); - } - } -} -function writeWorkspaceIntelligenceCli(intelligence) { - process.stdout.write(`Workspace intelligence v${intelligence.version}:\n`); - const files = intelligence.changedFiles; - process.stdout.write(`- Changed files: ${files.total} (${files.binary} binary, ${files.deleted} deleted, ${files.renamed} renamed)\n`); - process.stdout.write(`- Test evidence: ${intelligence.testEvidence.level}\n`); - if (intelligence.branch.pendingCommitCount > 0) { - process.stdout.write(`- Pending commits ahead of base: ${intelligence.branch.pendingCommitCount}\n`); - } - if (intelligence.baseFreshness.status !== "fresh") { - process.stdout.write(`- Base freshness: ${intelligence.baseFreshness.status}\n`); - for (const warning of intelligence.baseFreshness.warnings.slice(0, 2)) - process.stdout.write(` ${warning}\n`); - } - if (intelligence.blockers.branchQuality.length) { - process.stdout.write("- Branch-quality blockers:\n"); - for (const blocker of intelligence.blockers.branchQuality.slice(0, 4)) - process.stdout.write(` - ${blocker}\n`); - } - if (intelligence.blockers.accountState.length) { - process.stdout.write("- Account/queue blockers:\n"); - for (const blocker of intelligence.blockers.accountState.slice(0, 4)) - process.stdout.write(` - ${blocker}\n`); - } - if (intelligence.ciStatusHints.length) { - process.stdout.write("- CI hints:\n"); - for (const hint of intelligence.ciStatusHints.slice(0, 3)) - process.stdout.write(` - ${hint}\n`); - } - process.stdout.write(`- Rerun when: ${intelligence.rerunWhen}\n`); -} -function publicSafeWorkspaceIntelligence(intelligence) { - if (!intelligence) - return intelligence; - return { - ...intelligence, - blockers: { - ...intelligence.blockers, - accountState: [], - }, - rerunWhen: publicSafeRerunWhen(intelligence), - }; -} -function publicSafeRerunWhen(intelligence) { - if (intelligence.baseFreshness?.status === "stale" || intelligence.baseFreshness?.status === "possibly_stale") { - return "Run `git fetch origin` and rerun; current diff size may be inflated by stale base state."; - } - if (intelligence.blockers?.branchQuality?.length) { - return "Rerun after fixing branch-quality blockers or adding explicit validation/linked-context evidence."; - } - return "Rerun after any branch, base, or PR state changes before opening/submitting."; -} -function requirePublicSafePacketMarkdown(markdown) { - const unsafeLine = markdown.split(/\r?\n/).find((line) => isUnsafePublicPacketText(line)); - if (unsafeLine) - throw new Error("Refusing to print unsafe public packet markdown from the server."); - return markdown; -} -function isUnsafePublicPacketText(value) { - return /\b(reward\w*|score\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i.test(value); -} -function printVersion(options) { - const payload = { name: packageName, version: packageVersion, apiVersion: currentApiVersion, node: process.version }; - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`${packageName}/${packageVersion} (api ${currentApiVersion}, node ${process.version})\n`); -} -function toolsCommand(args) { - const subcommand = args[0]; - if (subcommand === "search") - return toolsSearchCommand(args.slice(1)); - const options = parseOptions(args); - const tools = STDIO_TOOL_DESCRIPTORS.map(({ name, category, description }) => ({ name, category, description })); - // Group tools by category in the canonical order; any category with no tools is omitted, and a tool - // whose category is unknown falls into a trailing "Other" bucket so nothing is silently dropped. - const knownIds = new Set(STDIO_TOOL_CATEGORIES.map((entry) => entry.id)); - const groups = [ - ...STDIO_TOOL_CATEGORIES.map((entry) => ({ ...entry, tools: tools.filter((tool) => tool.category === entry.id) })), - { id: "other", label: "Other", tools: tools.filter((tool) => !knownIds.has(tool.category)) }, - ].filter((group) => group.tools.length > 0); - if (options.json) { - const payload = { - count: tools.length, - categories: groups.map((group) => ({ id: group.id, label: group.label, count: group.tools.length })), - tools, - }; - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - const nameWidth = tools.reduce((width, tool) => Math.max(width, tool.name.length), 0); - groups.forEach((group, index) => { - if (index > 0) - process.stdout.write("\n"); - process.stdout.write(`${group.label} (${group.tools.length})\n`); - for (const tool of group.tools) { - process.stdout.write(` ${tool.name.padEnd(nameWidth)} ${tool.description}\n`); - } - }); -} -// `tools search ` — fuzzy discovery across the ~150-tool combined surface (#6300). Matches the -// query against each registered tool's name AND description (not name-only), so "stake" surfaces -// get_subnet_stake_quote even though "stake" is only in its description. Reuses this CLI's existing -// levenshteinDistance for typo tolerance rather than pulling in a fuzzy-match dependency. -function toolsSearchCommand(args) { - const options = parseOptions(args); - const query = args.find((arg) => !arg.startsWith("--")); - if (!query) - throw new Error("Usage: loopover-mcp tools search [--json]"); - const tools = searchTools(query); - const payload = { query, count: tools.length, tools }; - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - if (tools.length === 0) { - process.stdout.write(`No tools match "${query}".\n`); - return; - } - printToolRows(tools); -} -function printToolRows(tools) { - const nameWidth = tools.reduce((width, tool) => Math.max(width, tool.name.length), 0); - for (const tool of tools) { - process.stdout.write(`${tool.name.padEnd(nameWidth)} ${tool.description}\n`); - } -} -// Rank registered tools by how well they match the query, best first. A substring hit on the name beats -// a substring hit on the description, which beats a typo-tolerant (Levenshtein) hit on any name/description -// token; tools that match none of these are dropped. Ties break alphabetically for a stable listing. -function searchTools(query) { - const needle = query.toLowerCase(); - const scored = []; - for (const { name, description } of STDIO_TOOL_DESCRIPTORS) { - const score = scoreToolMatch(needle, name.toLowerCase(), description.toLowerCase()); - if (score !== null) - scored.push({ name, description, score }); - } - scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)); - return scored.map(({ name, description }) => ({ name, description })); -} -function scoreToolMatch(needle, name, description) { - if (name.includes(needle)) - return 0; - if (description.includes(needle)) - return 1; - // Typo tolerance: compare the query to each name/description token, allowing a small edit distance that - // scales with the query length (a longer query tolerates more typos, a very short one stays exact-ish). - const budget = Math.max(1, Math.floor(needle.length / 4)); - let best = Infinity; - for (const token of `${name} ${description}`.split(/[^a-z0-9]+/)) { - if (!token) - continue; - const distance = levenshteinDistance(needle, token); - if (distance < best) - best = distance; - } - return best <= budget ? 2 + best : null; -} -function completionCommand(args) { - const shell = args[0] && !args[0].startsWith("--") ? args[0] : undefined; - const options = parseOptions(args.filter((arg) => arg.startsWith("--"))); - if (!shell) - throw new Error(`Usage: loopover-mcp completion <${COMPLETION_SHELLS.join("|")}> [--json]`); - if (!COMPLETION_SHELLS.includes(shell)) - throw new Error(`Unsupported shell: ${shell}. Supported shells: ${COMPLETION_SHELLS.join(", ")}.`); - const script = buildCompletionScript(shell); - if (options.json) { - process.stdout.write(`${JSON.stringify({ shell, script }, null, 2)}\n`); - return; - } - process.stdout.write(`${script}\n`); -} -function buildCompletionScript(shell) { - const topLevel = [...Object.keys(CLI_COMMAND_SPEC), "help"]; - const withSubcommands = Object.entries(CLI_COMMAND_SPEC).filter(([, subcommands]) => subcommands.length > 0); - if (shell === "bash") - return buildBashCompletion(topLevel, withSubcommands); - if (shell === "zsh") - return buildZshCompletion(topLevel, withSubcommands); - if (shell === "fish") - return buildFishCompletion(topLevel, withSubcommands); - return buildPowershellCompletion(topLevel, withSubcommands); -} -// Suggest the closest known command for a typo, so an unknown command can offer a "did you mean". -// Only suggests within a small edit-distance budget that scales with input length, so unrelated -// input gets no (misleading) suggestion. -function suggestCommand(input) { - let best = null; - let bestDistance = Infinity; - for (const candidate of Object.keys(CLI_COMMAND_SPEC)) { - const distance = levenshteinDistance(input, candidate); - if (distance < bestDistance) { - bestDistance = distance; - best = candidate; - } - } - const budget = Math.max(2, Math.floor(input.length / 3)); - return best !== null && bestDistance > 0 && bestDistance <= budget ? best : null; -} -function levenshteinDistance(a, b) { - if (a.length === 0) - return b.length; - if (b.length === 0) - return a.length; - let previous = Array.from({ length: b.length + 1 }, (_, index) => index); - for (let i = 1; i <= a.length; i += 1) { - const current = [i]; - for (let j = 1; j <= b.length; j += 1) { - const cost = a[i - 1] === b[j - 1] ? 0 : 1; - current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost); - } - previous = current; - } - return previous[b.length]; -} -function buildBashCompletion(topLevel, withSubcommands) { - const subcommandCases = withSubcommands - .map(([command, subcommands]) => ` ${command}) COMPREPLY=( $(compgen -W "${subcommands.join(" ")}" -- "$cur") ); return 0;;`) - .join("\n"); - return `# loopover-mcp bash completion. Add to ~/.bashrc: -# source <(loopover-mcp completion bash) -_loopover_mcp() { - local cur prev cword - cur="\${COMP_WORDS[COMP_CWORD]}" - prev="\${COMP_WORDS[COMP_CWORD-1]}" - cword=\$COMP_CWORD - local commands="${topLevel.join(" ")}" - if [ "\$cword" -eq 1 ]; then - COMPREPLY=( $(compgen -W "\$commands --help --version" -- "$cur") ) - return 0 - fi - case "\${COMP_WORDS[1]}" in -${subcommandCases} - *) COMPREPLY=( $(compgen -W "--json --format --login --repo --profile --agent-profile --base --cwd" -- "$cur") ); return 0;; - esac -} -complete -F _loopover_mcp loopover-mcp`; -} -function buildZshCompletion(topLevel, withSubcommands) { - const subcommandCases = withSubcommands - .map(([command, subcommands]) => ` ${command}) _values 'subcommand' ${subcommands.join(" ")} ;;`) - .join("\n"); - return `#compdef loopover-mcp -# loopover-mcp zsh completion. Add to your fpath, or: -# source <(loopover-mcp completion zsh) -_loopover_mcp() { - local -a commands - commands=(${topLevel.join(" ")}) - if (( CURRENT == 2 )); then - _describe 'command' commands - return - fi - case $words[2] in -${subcommandCases} - esac -} -_loopover_mcp "$@"`; -} -function buildFishCompletion(topLevel, withSubcommands) { - const topLevelLines = topLevel - .map((command) => `complete -c loopover-mcp -n __fish_use_subcommand -a ${command} -d 'loopover-mcp command'`) - .join("\n"); - const subcommandLines = withSubcommands - .map(([command, subcommands]) => `complete -c loopover-mcp -n '__fish_seen_subcommand_from ${command}' -a '${subcommands.join(" ")}'`) - .join("\n"); - return `# loopover-mcp fish completion. Save to: -# ~/.config/fish/completions/loopover-mcp.fish -${topLevelLines} -${subcommandLines}`; -} -function buildPowershellCompletion(topLevel, withSubcommands) { - const commandList = topLevel.map((command) => `'${command}'`).join(", "); - const subcommandEntries = withSubcommands - .map(([command, subcommands]) => ` '${command}' = @(${subcommands.map((subcommand) => `'${subcommand}'`).join(", ")})`) - .join("\n"); - return `# loopover-mcp PowerShell completion. Add to your $PROFILE: -# loopover-mcp completion powershell | Out-String | Invoke-Expression -Register-ArgumentCompleter -Native -CommandName loopover-mcp -ScriptBlock { - param($wordToComplete, $commandAst, $cursorPosition) - $commands = @(${commandList}) - $subcommands = @{ -${subcommandEntries} - } - $elements = $commandAst.CommandElements - if ($elements.Count -le 2) { - $commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } - return - } - $sub = $subcommands[[string]$elements[1].Value] - if ($sub) { - $sub | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } - } -}`; -} -function printHelp() { - process.stdout.write(`Usage: - loopover-mcp --stdio - loopover-mcp version [--json] - loopover-mcp tools [--json] - loopover-mcp tools search [--json] - loopover-mcp completion bash|zsh|fish|powershell [--json] - loopover-mcp login [--profile name] [--github-token ] [--json] - loopover-mcp logout [--profile name] [--all] [--json] - loopover-mcp whoami [--profile name] [--json] - loopover-mcp config [--profile name] [--json] - loopover-mcp status [--profile name] [--json] - loopover-mcp telemetry enable|disable|status [--json] - loopover-mcp profile list|create|switch|remove [name] [--json] - loopover-mcp changelog [--json] - loopover-mcp doctor [--profile name] [--cwd path] [--exit-code] [--json] - loopover-mcp cache status|list|clear [--json] - loopover-mcp init-client --print codex|claude|cursor|mcp|vscode [--agent-profile miner-planner|maintainer-triage|repo-owner-intake] [--json] - loopover-mcp maintain status|queue|approve|reject|pause|resume|set-level|precision|outcome-calibration|onboarding-pack|audit-feed|automation-state|refresh-docs|generate-issue-drafts --repo owner/repo [--json] (see \`loopover-mcp maintain --help\`) - loopover-mcp decision-pack --login [--json] - loopover-mcp repo-decision --login --repo owner/repo [--json] - loopover-mcp contributor-profile [--login ] [--json] - loopover-mcp monitor-open-prs --login [--json] - loopover-mcp pr-outcomes --login [--limit N] [--json] - loopover-mcp explain-review-risk --repo owner/repo --title [--login ] [--body ] [--json] - loopover-mcp notifications --login [--json] - loopover-mcp notifications-read --login [--id ]... [--json] - loopover-mcp watch [owner/repo] [--labels a,b] [--login ] [--json] - loopover-mcp analyze-branch --login [--repo owner/repo] [--base origin/main] [--branch-eligibility eligible|ineligible|unknown] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--scenario-note "..."] [--validation "passed|npm test|summary"] [--format table] [--json] - loopover-mcp preflight --login [--repo owner/repo] [--base origin/main] [--branch-eligibility eligible|ineligible|unknown] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--validation "passed|npm test|summary"] [--format table] [--json] - loopover-mcp review-pr --login [--repo owner/repo] [--base origin/main] [--commit ]... [--body ] [--body-file ] [--linked-issue ] [--json] - loopover-mcp lint-pr-text [--commit ]... [--body ] [--body-file ] [--linked-issue ] [--json] - loopover-mcp validate-config --file [--source repo_file|api_record|none] [--json] - loopover-mcp slop-risk [--description ] [--description-file ] [--changed-file ]... [--test ]... [--test-file ]... [--json] - loopover-mcp improvement-potential [--changed-file ]... [--test ]... [--test-file ]... [--patch-coverage-delta ] [--json] - loopover-mcp issue-slop [--title ] [--body ] [--body-file ] [--json] - loopover-mcp agent plan --login [--repo owner/repo] [--json] - loopover-mcp agent status [--json] - loopover-mcp agent explain [--json] - loopover-mcp agent packet --login [--repo owner/repo] [--base origin/main] [--json] - - Environment: - LOOPOVER_API_URL - LOOPOVER_PROFILE - LOOPOVER_CONFIG_PATH or LOOPOVER_CONFIG_DIR - LOOPOVER_API_TOKEN, LOOPOVER_MCP_TOKEN, LOOPOVER_TOKEN, or a session from loopover-mcp login - LOOPOVER_LOGIN or GITHUB_LOGIN (default --login for analyze-branch, preflight, review-pr, decision-pack, repo-decision, monitor-open-prs, pr-outcomes, notifications, notifications-read, and agent plan/packet) - GITHUB_TOKEN for non-interactive login bootstrap - GITTENSOR_SCORE_PREVIEW_CMD - GITTENSOR_ROOT - GITTENSOR_SCORE_PREVIEW_TIMEOUT_MS - LOOPOVER_UPLOAD_SOURCE=false -`); -} -function printCacheHelp() { - process.stdout.write(`Usage: - loopover-mcp cache status [--json] - loopover-mcp cache list [--json | --format ndjson] - loopover-mcp cache clear [--json] - -Decision-pack cache entries are local-only stale fallbacks for temporary API/network outages. -Source upload remains disabled. -`); -} -function printAgentHelp() { - process.stdout.write(`Usage: - loopover-mcp agent plan --login [--repo owner/repo] [--objective "..."] [--json] - loopover-mcp agent status [--json] - loopover-mcp agent explain [--json] - loopover-mcp agent packet --login [--repo owner/repo] [--base origin/main] [--validation "passed|command|summary"] [--json] - -The agent is copilot-only: it ranks, explains, and drafts public-safe packets. It does not edit code, open PRs, or post comments from the local MCP wrapper. -Source upload remains disabled. - `); -} -function printProfileHelp() { - process.stdout.write(`Usage: - loopover-mcp profile list [--json | --format ndjson] - loopover-mcp profile create [--json] - loopover-mcp profile switch [--json] - loopover-mcp profile remove [--json] - -Use --profile or LOOPOVER_PROFILE to run login, logout, whoami, status, doctor, and MCP API calls with a named local session. -`); -} -function parseOptions(args) { - const options = {}; - const repeatable = new Set(["label", "issue", "id", "commit", "changedFile", "test", "testFile", "validation", "validationCommand", "validationStatus", "validationSummary", "validationDuration", "scenarioNote"]); - for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; - if (arg === "--json") { - options.json = true; - continue; - } - if (!arg?.startsWith("--")) { - // A bare `help` positional means the same thing as `--help` (#6257): the option-consuming commands - // (decision-pack/repo-decision/review-pr) only check `options.help === true`, so without this a - // dashless `loopover-mcp decision-pack help` fell through to a confusing "Pass --login…" error instead - // of printing usage — while the raw-args commands (lint-pr-text etc.) already special-cased it. A `help` - // consumed as a `--key value` value is skipped via `index += 1` below, so only a STANDALONE `help` here. - if (arg === "help") - options.help = true; - continue; - } - // Support the inline `--key=value` form (e.g. `--format=table`) alongside the space-separated - // `--key value` form; splitting here keeps every existing space-separated option unchanged (#2231). - const equals = arg.indexOf("="); - if (equals !== -1) { - const inlineKey = camel(arg.slice(2, equals)); - const inlineValue = arg.slice(equals + 1); - if (repeatable.has(inlineKey)) - options[inlineKey] = [...(options[inlineKey] ?? []), inlineValue]; - else - options[inlineKey] = inlineValue; - continue; - } - const key = camel(arg.slice(2)); - const value = args[index + 1]; - if (!value || value.startsWith("--")) { - options[key] = true; - continue; - } - index += 1; - if (repeatable.has(key)) - options[key] = [...(options[key] ?? []), value]; - else - options[key] = value; - } - return options; -} -// Shared machine-readable output for list-shaped commands. `--format ndjson` streams one JSON object per -// array element per line (for piping into jq/log processors); `--json` (or `--format json`) keeps the -// existing pretty object. Returns true when it emitted a machine-readable format, so the caller skips the -// human view. Each record ends in "\n" and Node flushes stdout on exit, so piped output is not truncated. -function emitList(options, items, pretty) { - if (options.format === "ndjson") { - for (const item of items) - process.stdout.write(`${JSON.stringify(item)}\n`); - return true; - } - if (options.json || options.format === "json") { - process.stdout.write(`${JSON.stringify(pretty, null, 2)}\n`); - return true; - } - return false; -} -async function login(options) { - const profileName = selectedProfileName(options); - const githubToken = options.githubToken ?? process.env.GITHUB_TOKEN; - const session = githubToken ? await apiFetch("/v1/auth/github/session", { method: "POST", body: JSON.stringify({ githubToken }) }, { auth: false }) : await loginWithDeviceFlow(); - const nextConfig = upsertProfile(config, profileName, { - apiUrl, - session: { - token: session.token, - login: session.login, - expiresAt: session.expiresAt, - scopes: session.scopes ?? [], - }, - }); - saveConfig(nextConfig); - const payload = { status: "authenticated", profile: profileName, login: session.login, apiUrl, expiresAt: session.expiresAt }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Authenticated profile ${profileName} as ${session.login}. Session expires ${session.expiresAt}.\n`); -} -async function loginWithDeviceFlow() { - const start = await apiFetch("/v1/auth/github/device/start", { method: "POST", body: "{}" }, { auth: false }); - process.stderr.write(`Open ${start.verificationUri} and enter code ${start.userCode}.\n`); - const deadline = Date.now() + Number(start.expiresIn ?? 900) * 1000; - let intervalMs = Math.max(5, Number(start.interval ?? 5)) * 1000; - while (Date.now() < deadline) { - await sleep(intervalMs); - let result; - try { - result = await apiFetch("/v1/auth/github/device/poll", { method: "POST", body: JSON.stringify({ deviceCode: start.deviceCode }) }, { auth: false }); - } - catch (error) { - // A transient 429 from our own rate limiter (#6792) is not a GitHub-reported device-flow status -- - // back off using the server's Retry-After and keep polling within the deadline, the same posture - // already applied to GitHub's own "slow_down" status below, instead of aborting the whole attempt. - if (error?.status === 429) { - const retryAfterSeconds = Number(/retry-after=(\d+)s/.exec(error.message)?.[1]); - intervalMs = Math.max(intervalMs, (Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : 5) * 1000); - continue; - } - throw error; - } - if (result.token) - return result; - if (result.status === "slow_down") - intervalMs += 5000; - if (result.status && result.status !== "authorization_pending" && result.status !== "slow_down") - throw new Error(`GitHub OAuth failed: ${result.status}`); - } - throw new Error("GitHub OAuth device flow expired."); -} -async function logout(options) { - const profileName = selectedProfileName(options); - const all = options.all === true; - const envToken = getEnvApiToken(); - const tokens = all - ? [envToken, ...profileSessions(config).map((entry) => entry.session.token)].filter(Boolean) - : [envToken ?? configuredProfileToken(profileName)].filter(Boolean); - const remote = []; - for (const token of [...new Set(tokens)]) { - try { - remote.push(await apiFetch("/v1/auth/logout", { method: "POST", body: "{}" }, { token })); - } - catch (error) { - remote.push({ error: sanitizeDiagnosticText(error instanceof Error ? error.message : "logout_failed") }); - } - } - const nextConfig = all ? clearAllProfileSessions(config) : clearProfileSession(config, profileName); - if (hasPersistedConfigState(nextConfig)) - saveConfig(nextConfig); - else if (existsSync(configPath)) - rmSync(configPath, { force: true }); - const decisionPackCache = clearDecisionPackCache(); - const payload = { status: "logged_out", profile: all ? "all" : profileName, apiUrl, remote: remote.length > 0 ? remote : null, decisionPackCache }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(all ? "Logged out all profiles.\n" : `Logged out profile ${profileName}.\n`); -} -// Local MCP usage telemetry is opt-in and defaults OFF (#6239, per #6228's privacy decision): a -// self-hoster must explicitly enable it before anything is measured. The opt-in is a single top-level -// `telemetryEnabled` flag persisted in the same config file `login` uses, so the choice survives across -// CLI invocations; `status`, `doctor`, and `config` all report the current state. -function telemetryCommand(args) { - const subcommand = args[0] ?? "status"; - const options = parseOptions(args.slice(1)); - if (subcommand === "--help" || subcommand === "help") - return printTelemetryHelp(); - if (subcommand === "enable" || subcommand === "disable") { - const enabled = subcommand === "enable"; - const nextConfig = setTelemetryEnabled(config, enabled); - // Mirror login/logout persistence: keep the file when any durable state remains, otherwise remove it - // so disabling telemetry on an otherwise-empty config leaves no stray file behind. - if (hasPersistedConfigState(nextConfig)) - saveConfig(nextConfig); - else if (existsSync(configPath)) - rmSync(configPath, { force: true }); - const payload = { status: enabled ? "telemetry_enabled" : "telemetry_disabled", telemetry: telemetryState(nextConfig) }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(enabled ? "Local MCP usage telemetry enabled.\n" : "Local MCP usage telemetry disabled.\n"); - return; - } - if (subcommand === "status") { - const telemetry = telemetryState(config); - if (options.json) - process.stdout.write(`${JSON.stringify({ telemetry }, null, 2)}\n`); - else - process.stdout.write(`Telemetry: ${telemetry.enabled ? "enabled (opt-in)" : "disabled (default)"}\n`); - return; - } - throw new Error(`Unknown telemetry command: ${subcommand}. Use enable | disable | status.`); -} -function printTelemetryHelp() { - process.stdout.write(`Usage: - loopover-mcp telemetry status [--json] - loopover-mcp telemetry enable [--json] - loopover-mcp telemetry disable [--json] - -Local MCP usage telemetry is opt-in and defaults OFF. Enabling it persists a top-level telemetryEnabled -flag in the same config file \`loopover-mcp login\` uses, so the choice survives across CLI invocations. -\`status\`, \`doctor\`, and \`config\` report the current opt-in state. -`); -} -function profileCommand(args) { - const subcommand = args[0] ?? "list"; - const options = parseOptions(args.slice(1)); - if (subcommand === "--help" || subcommand === "help") - return printProfileHelp(); - if (subcommand === "list" || subcommand === "ls") { - const profiles = profileList(config); - const payload = { activeProfile: activeProfileName, profiles }; - if (emitList(options, profiles, payload)) - return; - process.stdout.write(`Active profile: ${activeProfileName}\n`); - for (const profile of profiles) { - process.stdout.write(`- ${profile.name}${profile.active ? " (active)" : ""}: ${profile.login ?? "not authenticated"}\n`); - } - return; - } - const rawName = args[1] && !args[1].startsWith("--") ? args[1] : options.name ?? options.profile; - if (!rawName) - throw new Error(`Usage: loopover-mcp profile ${subcommand} `); - const profileName = normalizeProfileName(rawName); - if (subcommand === "create") { - const nextConfig = ensureProfile(config, profileName, { activate: true }); - saveConfig(nextConfig); - const payload = { status: "created", activeProfile: profileName, profile: profilePublicState(profileName, nextConfig) }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Created and selected profile ${profileName}.\n`); - return; - } - if (subcommand === "switch" || subcommand === "use") { - if (!config.profiles?.[profileName]) - throw new Error(`Profile ${profileName} does not exist. Run \`loopover-mcp profile create ${profileName}\` or \`loopover-mcp login --profile ${profileName}\`.`); - const nextConfig = setActiveProfile(config, profileName); - saveConfig(nextConfig); - const payload = { status: "switched", activeProfile: profileName, profile: profilePublicState(profileName, nextConfig) }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Selected profile ${profileName}.\n`); - return; - } - if (subcommand === "remove" || subcommand === "rm" || subcommand === "delete") { - const nextConfig = removeProfile(config, profileName); - if (hasPersistedConfigState(nextConfig)) - saveConfig(nextConfig); - else if (existsSync(configPath)) - rmSync(configPath, { force: true }); - const payload = { status: "removed", removedProfile: profileName, activeProfile: nextConfig.activeProfile ?? defaultProfileName }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(`Removed profile ${profileName}.\n`); - return; - } - throw new Error(`Unknown profile command: ${subcommand}`); -} -async function whoami(options) { - const payload = { ...(await apiGet("/v1/auth/session")), profile: activeProfileName }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(activeProfileName === defaultProfileName ? `${payload.login}\n` : `${payload.login} (profile ${activeProfileName})\n`); -} -async function status(options) { - let auth = { status: getApiToken() ? "token_configured" : "unauthenticated" }; - let health = null; - if (getApiToken()) { - try { - auth = await apiGet("/v1/auth/session"); - } - catch (error) { - auth = { status: "token_configured", session: "unverified", error: sanitizeDiagnosticText(error instanceof Error ? error.message : "status_failed") }; - } - } - try { - health = await apiFetch("/health", { method: "GET" }, { auth: false, timeoutMs: 5000 }); - } - catch (error) { - health = { status: "unreachable", error: sanitizeDiagnosticText(error instanceof Error ? error.message : "health_check_failed") }; - } - const compatibility = await inspectApiCompatibility(health); - const pkg = await inspectInstallVersion(compatibilityLatestRecommendedVersion(compatibility.report) ?? compatibilityLatestRecommendedVersion(health)); - const apiCompatibility = compatibility.evaluation; - const decisionPackCache = inspectDecisionPackCache(); - const payload = { - apiUrl, - package: pkg, - apiCompatibility, - compatibility: compatibility.report, - api: health, - auth, - profile: profilePublicState(activeProfileName), - config: { configured: existsSync(configPath), activeProfile: activeProfileName, profileCount: profileList(config).length }, - decisionPackCache, - sourceUploadDefault: false, - sourceUploadSupported: false, - telemetry: telemetryState(), - }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else { - process.stdout.write(`${packageName}: ${packageVersion}${pkg.latestVersion ? ` (latest ${pkg.latestVersion})` : ""}\n`); - process.stdout.write(`API: ${apiUrl}\n`); - process.stdout.write(`Profile: ${activeProfileName}\n`); - process.stdout.write(`API health: ${health?.status ?? "unknown"}\n`); - process.stdout.write(`Auth: ${auth.status}${auth.login ? ` (${auth.login})` : ""}\n`); - process.stdout.write(`Decision-pack cache: ${decisionPackCache.entries} entr${decisionPackCache.entries === 1 ? "y" : "ies"}\n`); - process.stdout.write("Source upload: disabled\n"); - process.stdout.write(`Telemetry: ${payload.telemetry.enabled ? "enabled (opt-in)" : "disabled (default)"}\n`); - if (pkg.state === "stale") { - process.stdout.write(`Update available: ${packageVersion} -> ${pkg.latestVersion}. Upgrade with:\n ${pkg.upgradeCommand}\n`); - process.stdout.write(`Or run without installing:\n ${pkg.npxFallback}\n`); - } - else if (pkg.state === "unavailable") { - process.stdout.write("Version check: npm registry was unavailable; skipping update check.\n"); - } - if (apiCompatibility.status === "incompatible") { - process.stdout.write(`API requires at least ${packageName}@${apiCompatibility.minVersion}. Upgrade with:\n ${apiCompatibility.upgradeCommand}\n`); - } - else if (apiCompatibility.status === "compatible") { - process.stdout.write(`API compatibility: compatible (minimum ${packageName}@${apiCompatibility.minVersion}).\n`); - } - else if (apiCompatibility.status === "unavailable") { - process.stdout.write(`API compatibility: unavailable (${apiCompatibility.reason ?? "unknown"}).\n`); - } - else if (apiCompatibility.status === "unknown") { - // Mirror doctor()'s unknown arm (#6263): an unparseable minimum version must still surface in human output. - process.stdout.write(`API reported an unsupported minimum client version (${apiCompatibility.minVersion}).\n`); - } - } -} -async function changelog(options) { - const text = existsSync(changelogPath) ? readFileSync(changelogPath, "utf8") : "# Changelog\n\nNo packaged changelog was found.\n"; - const payload = { - package: { - name: packageName, - version: packageVersion, - }, - changelog: text, - }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(text.endsWith("\n") ? text : `${text}\n`); -} -async function doctor(options) { - const checks = []; - const add = (name, statusValue, detail, remediation) => checks.push(stripUndefined({ - name, - status: statusValue, - detail: sanitizeDiagnosticText(detail, [options.cwd]), - remediation: sanitizeDiagnosticText(remediation, [options.cwd]), - })); - let authLogin = options.login ?? activeProfile.session?.login; - let repoFullName = typeof options.repo === "string" ? options.repo : undefined; - let health = null; - try { - health = await apiFetch("/health", { method: "GET" }, { auth: false }); - add("api_health", health.status === "ok" ? "pass" : "warn", `API responded from ${apiUrl}.`); - } - catch (error) { - health = { status: "unreachable" }; - add("api_health", "fail", error instanceof Error ? error.message : "health_check_failed", "Check LOOPOVER_API_URL or network access."); - } - const compatibility = await inspectApiCompatibility(health); - const pkg = await inspectInstallVersion(compatibilityLatestRecommendedVersion(compatibility.report) ?? compatibilityLatestRecommendedVersion(health)); - if (pkg.state === "stale") { - add("version", "warn", `Installed ${packageVersion} is behind npm latest ${pkg.latestVersion}.`, `${pkg.upgradeCommand} (no-install fallback: ${pkg.npxFallback})`); - } - else if (pkg.state === "unavailable") { - add("version", "warn", "Could not reach the npm registry to check for updates.", `Retry when online, or run the no-install fallback: ${npxFallbackCommand}`); - } - else if (pkg.state === "unknown") { - add("version", "warn", `Could not compare local ${packageVersion} against npm latest ${pkg.latestVersion ?? "unknown"}.`); - } - else if (pkg.state === "ahead") { - add("version", "pass", `Installed ${packageVersion} is ahead of npm latest ${pkg.latestVersion}.`); - } - else if (pkg.state === "skipped") { - add("version", "pass", "npm version check was skipped (LOOPOVER_SKIP_NPM_VERSION_CHECK)."); - } - else { - add("version", "pass", `Installed ${packageVersion} matches npm latest ${pkg.latestVersion}.`); - } - const apiCompatibility = compatibility.evaluation; - if (apiCompatibility.status === "incompatible") { - add("api_compatibility", "fail", `API requires at least ${packageName}@${apiCompatibility.minVersion}; local is ${packageVersion}.`, apiCompatibility.upgradeCommand); - } - else if (apiCompatibility.status === "compatible") { - add("api_compatibility", "pass", `Local ${packageVersion} meets the API minimum ${apiCompatibility.minVersion}.`); - } - else if (apiCompatibility.reason === "api_unreachable") { - add("api_compatibility", "warn", "API compatibility check was unavailable because API health was unreachable."); - } - else if (apiCompatibility.reason === "compatibility_endpoint_unavailable") { - add("api_compatibility", "warn", "API compatibility endpoint was unavailable; compatibility could not be confirmed."); - } - else if (apiCompatibility.status === "unknown") { - add("api_compatibility", "warn", `API reported an unsupported minimum client version (${apiCompatibility.minVersion}).`); - } - else { - add("api_compatibility", "pass", "API did not report a minimum client version; compatibility check skipped."); - } - const token = getApiToken(); - if (!token) { - add("auth", "fail", `No LoopOver API/session token is configured for profile ${activeProfileName}.`, `Run \`loopover-mcp login --profile ${activeProfileName}\`.`); - } - else { - try { - const session = await apiGet("/v1/auth/session"); - authLogin = session.login ?? authLogin; - add("auth", "pass", `Profile ${activeProfileName} authenticated as ${session.login}; session expires ${session.expiresAt}.`); - } - catch (error) { - add("auth", "warn", `A token is configured for profile ${activeProfileName} but no user session was verified: ${error instanceof Error ? error.message : "session_check_failed"}.`, "If this is a static beta token, this can be expected. Otherwise run `loopover-mcp login`."); - } - } - if (/^(1|true|yes)$/i.test(process.env.LOOPOVER_UPLOAD_SOURCE ?? "false")) { - add("source_upload", "fail", "LOOPOVER_UPLOAD_SOURCE is enabled.", "Unset LOOPOVER_UPLOAD_SOURCE. Source upload is unsupported in v1."); - } - else { - add("source_upload", "pass", "Source upload is disabled and unsupported in v1."); - } - // Either telemetry stance is a valid, deliberate choice, so this is always a pass — it just makes the - // current opt-in visible (and points at the toggle) rather than gating the checklist. - const telemetry = telemetryState(); - add("telemetry", "pass", telemetry.enabled ? "Local MCP usage telemetry is enabled (opt-in)." : "Local MCP usage telemetry is disabled (default).", telemetry.enabled ? "Run `loopover-mcp telemetry disable` to opt back out." : "Run `loopover-mcp telemetry enable` to opt in."); - const decisionPackCache = inspectDecisionPackCache(); - add("decision_pack_cache", "pass", `Local stale fallback cache has ${decisionPackCache.entries} entr${decisionPackCache.entries === 1 ? "y" : "ies"} and is bounded at ${decisionPackCache.maxEntries}.`, "Run `loopover-mcp cache clear` to remove local stale fallback data."); - try { - const metadata = collectLocalBranchMetadata({ - cwd: options.cwd ?? process.cwd(), - baseRef: options.base, - repoFullName: options.repo, - login: options.login ?? activeProfile.session?.login ?? "local", - }); - repoFullName = metadata.repoFullName ?? repoFullName; - add("git_metadata", "pass", `${metadata.repoFullName} on ${metadata.branchName}; ${metadata.changedFiles.length} changed file(s).`); - } - catch (error) { - add("git_metadata", "warn", error instanceof Error ? error.message : "git_metadata_failed", "Run from a git repo or pass --repo owner/repo."); - } - const commandPath = findExecutable("loopover-mcp"); - if (commandPath) - add("client_path", "pass", "loopover-mcp is visible on PATH."); - else - add("client_path", "warn", "loopover-mcp was not found on PATH.", "Use an absolute command path in your MCP client config."); - const scorerCommand = resolveScorePreviewCommand(); - if (!scorerCommand) { - add("local_scorer", "warn", "GITTENSOR_SCORE_PREVIEW_CMD is not configured; branch analysis will fall back to metadata-only scoring.", `Example: export GITTENSOR_SCORE_PREVIEW_CMD="${referenceScorePreviewExample("metadata")}"`); - } - else { - const probe = probeLocalScorer(scorerCommand); - if (probe.ok) { - add("local_scorer", "pass", `Configured scorer responded in ${probe.durationMs ?? 0}ms.`); - } - else { - const remediation = setupGuidanceForLocalScorer(probe).slice(1).join(" "); - add("local_scorer", "warn", `Configured scorer failed (${probe.code ?? "scorer_failed"}): ${probe.reason}`, remediation || "Run loopover-mcp doctor --json for structured diagnostics."); - } - } - if (process.env.GITTENSOR_ROOT) { - add("gittensor_root", "pass", "GITTENSOR_ROOT is configured."); - } - else if (scorerCommand?.includes("gittensor-score-preview.py")) { - add("gittensor_root", "warn", "Python gittensor scorer is configured but GITTENSOR_ROOT is unset.", "Set GITTENSOR_ROOT to a local entrius/gittensor checkout."); - } - const statusValue = doctorStatus(checks); - const checklist = buildDoctorChecklist(checks, { - status: statusValue, - profileName: activeProfileName, - login: authLogin, - repoFullName, - }); - const nextCommand = checklist.find((group) => group.id === "next_command")?.nextCommand; - const payload = { - status: statusValue, - apiUrl, - profile: profilePublicState(activeProfileName), - config: { configured: existsSync(configPath), activeProfile: activeProfileName, profileCount: profileList(config).length }, - decisionPackCache, - sourceUploadSupported: false, - telemetry, - checklist, - nextCommand, - checks, - }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else { - process.stdout.write(`LoopOver doctor: ${payload.status}\n`); - process.stdout.write(`Profile: ${activeProfileName}\n`); - for (const group of checklist) { - process.stdout.write(`\n${group.title}: ${group.status}\n`); - if (group.id === "next_command") { - process.stdout.write(`- ${group.detail}\n`); - if (group.nextCommand?.command) - process.stdout.write(` ${group.nextCommand.command}\n`); - continue; - } - // #6261: a check's `detail` is the one field here that carries text this CLI didn't write -- an API error - // message, an npm-registry error, a compatibility report's `error`. Some of those already pass through - // sanitizeDiagnosticText, but that redacts tokens and local paths; it is indifferent to escape sequences. So - // the terminal pass belongs here at the print boundary, where it covers every check source at once. - for (const check of group.checks ?? []) { - process.stdout.write(`- ${sanitizePlainTextTerminalOutput(check.status)}: ${sanitizePlainTextTerminalOutput(check.name)} - ${sanitizePlainTextTerminalOutput(check.detail)}\n`); - if (check.remediation) - process.stdout.write(` ${sanitizePlainTextTerminalOutput(check.remediation)}\n`); - } - } - } - // Opt-in: let `doctor` gate CI/pre-commit by exiting non-zero when a check fails. The default - // stays exit 0 so existing scripts that ignore the exit code keep working. - return options.exitCode && payload.status === "needs_attention" ? 1 : 0; -} -function doctorStatus(checks) { - if (checks.some((check) => check.status === "fail")) - return "needs_attention"; - if (checks.some((check) => check.status === "warn")) - return "warnings"; - return "ok"; -} -function buildDoctorChecklist(checks, context) { - const byName = new Map(checks.map((check) => [check.name, check])); - const groups = doctorChecklistGroups().map((group) => { - const groupChecks = group.checks.map((name) => byName.get(name)).filter(Boolean); - return stripUndefined({ - id: group.id, - title: group.title, - status: checklistStatus(groupChecks), - checks: groupChecks, - }); - }); - const nextCommand = doctorNextCommand(byName, context); - return [ - ...groups, - stripUndefined({ - id: "next_command", - title: "Next command", - status: context.status === "needs_attention" ? "fail" : context.status === "warnings" ? "warn" : "pass", - detail: nextCommand.reason, - nextCommand, - }), - ]; -} -function doctorChecklistGroups() { - return [ - { id: "auth", title: "Auth", checks: ["auth"] }, - { id: "api_compatibility", title: "API compatibility", checks: ["api_health", "version", "api_compatibility"] }, - { id: "local_repo_readiness", title: "Local repo readiness", checks: ["git_metadata", "client_path"] }, - { id: "scorer_availability", title: "Scorer availability", checks: ["local_scorer", "gittensor_root"] }, - { id: "output_safety", title: "Output safety", checks: ["source_upload", "decision_pack_cache", "telemetry"] }, - ]; -} -function checklistStatus(checks) { - if (checks.some((check) => check.status === "fail")) - return "fail"; - if (checks.some((check) => check.status === "warn")) - return "warn"; - return "pass"; -} -function doctorNextCommand(byName, context) { - const sourceUpload = byName.get("source_upload"); - if (sourceUpload?.status === "fail") { - return { - command: "unset LOOPOVER_UPLOAD_SOURCE", - reason: "Disable source upload first; the local MCP wrapper only sends metadata.", - }; - } - const apiCompatibility = byName.get("api_compatibility"); - if (apiCompatibility?.status === "fail") { - return { - command: apiCompatibility.remediation ?? upgradeCommand, - reason: "Upgrade the MCP package before relying on API-backed commands.", - }; - } - const auth = byName.get("auth"); - if (auth?.status === "fail") { - return { - command: `loopover-mcp login --profile ${shellArg(context.profileName ?? "default")}`, - reason: "Authenticate the active profile so doctor, plan, preflight, and packet commands can call the API.", - }; - } - const apiHealth = byName.get("api_health"); - if (apiHealth?.status === "fail") { - return { - command: "loopover-mcp status --json", - reason: "Check API reachability before running planner or preflight commands.", - }; - } - const version = byName.get("version"); - if (version?.status === "warn" && version.remediation?.includes("npm install")) { - return { - command: upgradeCommand, - reason: "Update the MCP package so local behavior matches the current API.", - }; - } - const gitMetadata = byName.get("git_metadata"); - if (gitMetadata?.status === "warn") { - return { - command: "loopover-mcp doctor --repo owner/repo --json", - reason: "Run doctor from a git checkout or pass the repository explicitly.", - }; - } - const localScorer = byName.get("local_scorer"); - if (localScorer?.status === "warn" && localScorer.remediation) { - const scorerSetupCommand = localScorer.remediation.startsWith("Example: ") ? localScorer.remediation.replace(/^Example:\s*/, "") : "loopover-mcp doctor --json"; - return { - command: scorerSetupCommand, - reason: "Configure the optional local scorer for richer private branch analysis.", - }; - } - return { - command: `loopover-mcp review-pr --login ${shellArg(context.login ?? "")} --repo ${shellArg(context.repoFullName ?? "owner/repo")} --json`, - reason: "Run the composed pre-PR review (preflight + slop-risk + PR-text lint) next; source upload remains disabled.", - }; -} -function shellArg(value) { - const text = String(value ?? ""); - if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) - return text; - return `'${text.replace(/'/g, `'"'"'`)}'`; -} -function initClient(options) { - const client = String(options.print ?? options.client ?? "").toLowerCase(); - if (!client) - throw new Error("Pass --print codex, --print claude, --print cursor, --print mcp, or --print vscode."); - const command = options.command ?? "loopover-mcp"; - const snippet = clientSnippet(client, command); - const agentProfile = resolveAgentProfile(options.agentProfile); - const payload = { - client, - command, - args: ["--stdio"], - snippet, - agentProfile, - notes: [ - "Run `loopover-mcp login` before starting the MCP client.", - "Use an absolute command path if the client does not inherit your shell PATH.", - "This command prints config only; it does not edit client files.", - ...(agentProfile - ? [ - agentProfile.drivingLoop - ? `Use the ${agentProfile.title} profile instructions as the agent system/developer prompt. Every GitHub write runs LOCALLY via your harness with your own credentials, only after the LoopOver gate + anti-slop check pass — LoopOver never performs the write.` - : `Use the ${agentProfile.title} profile instructions as the agent system/developer prompt; keep all GitHub writes human-approved.`, - ] - : []), - ], - }; - if (options.json) - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - else - process.stdout.write(agentProfile ? `${snippet}\n\n${formatAgentProfile(agentProfile)}\n` : `${snippet}\n`); -} -function resolveAgentProfile(profileId) { - if (!profileId) - return null; - const id = String(profileId).trim().toLowerCase(); - if (!Object.hasOwn(AGENT_PROFILES, id)) - throw new Error(`Unsupported agent profile: ${profileId}. Use ${AGENT_PROFILE_IDS.join(", ")}.`); - return AGENT_PROFILES[id]; -} -function formatAgentProfile(profile) { - return [ - `# LoopOver agent profile: ${profile.title}`, - `Audience: ${profile.audience}`, - `Purpose: ${profile.purpose}`, - "", - "Recommended MCP prompts:", - ...profile.recommendedPrompts.map((name) => `- ${name}`), - "", - "Recommended MCP tools:", - ...profile.recommendedTools.map((name) => `- ${name}`), - ...(profile.drivingLoop ? ["", "Driving loop (plan → implement → push, gate-throttled):", ...profile.drivingLoop.map((step, index) => `${index + 1}. ${step}`)] : []), - "", - "Safety boundaries:", - ...profile.boundaries.map((boundary) => `- ${boundary}`), - "", - `When not to use: ${profile.whenNotToUse}`, - ].join("\n"); -} -function getApiToken() { - return getEnvApiToken() ?? configuredProfileToken(activeProfileName); -} -function getEnvApiToken() { - // Precedence matches the documented order (README, printHelp, the missing-auth error, and the - // sanitizer list): the MCP-specific token wins over the generic LOOPOVER_TOKEN, which previously - // took priority here and contradicted every other reference to this order. - return process.env.LOOPOVER_API_TOKEN ?? process.env.LOOPOVER_MCP_TOKEN ?? process.env.LOOPOVER_TOKEN; -} -function selectedProfileName(options = {}) { - return normalizeProfileName(options.profile ?? activeProfileName); -} -function configuredProfileToken(profileName, currentConfig = config) { - return currentConfig.profiles?.[profileName]?.session?.token; -} -function profileSessions(currentConfig = config) { - return Object.entries(currentConfig.profiles ?? {}) - .flatMap(([name, profile]) => (profile?.session?.token ? [{ name, session: profile.session }] : [])); -} -function profilePublicState(profileName, currentConfig = config) { - const profile = currentConfig.profiles?.[profileName]; - const hasEnvToken = Boolean(getEnvApiToken()); - return { - name: profileName, - active: profileName === (currentConfig.activeProfile ?? defaultProfileName), - configured: Boolean(profile), - authenticated: Boolean(profile?.session?.token), - login: profile?.session?.login ?? null, - expiresAt: profile?.session?.expiresAt ?? null, - tokenSource: hasEnvToken ? "environment" : profile?.session?.token ? "profile" : "none", - apiUrl: profile?.apiUrl ?? currentConfig.apiUrl ?? null, - }; -} -function profileList(currentConfig = config) { - const names = new Set([defaultProfileName, currentConfig.activeProfile ?? defaultProfileName, ...Object.keys(currentConfig.profiles ?? {})]); - return [...names].sort((left, right) => (left === currentConfig.activeProfile ? -1 : right === currentConfig.activeProfile ? 1 : left.localeCompare(right))).map((name) => profilePublicState(name, currentConfig)); -} -function selectProfileName(currentConfig, requestedName) { - const requested = requestedName ? normalizeProfileName(requestedName) : undefined; - if (requested) - return requested; - const configured = currentConfig?.activeProfile ? normalizeProfileName(currentConfig.activeProfile) : defaultProfileName; - if (currentConfig?.profiles?.[configured]) - return configured; - return currentConfig?.profiles?.[defaultProfileName] || configured === defaultProfileName ? defaultProfileName : configured; -} -function resolvedApiUrlSource() { - if (process.env.LOOPOVER_API_URL) - return "environment"; - const profileApiUrl = typeof activeProfile.apiUrl === "string" ? activeProfile.apiUrl.replace(/\/+$/, "") : undefined; - if (profileApiUrl && !legacyDefaultApiUrls.has(profileApiUrl)) - return "profile"; - const globalApiUrl = typeof config.apiUrl === "string" ? config.apiUrl.replace(/\/+$/, "") : undefined; - if (globalApiUrl && !legacyDefaultApiUrls.has(globalApiUrl)) - return "config"; - return "default"; -} -function resolvedConfigPathSource() { - if (process.env.LOOPOVER_CONFIG_PATH) - return "LOOPOVER_CONFIG_PATH"; - if (process.env.LOOPOVER_CONFIG_DIR) - return "LOOPOVER_CONFIG_DIR"; - if (process.env.XDG_CONFIG_HOME) - return "XDG_CONFIG_HOME"; - return "default"; -} -function resolvedTokenSource() { - if (getEnvApiToken()) - return "environment"; - if (configuredProfileToken(activeProfileName)) - return "profile"; - return "none"; -} -function sourceUploadState() { - const enabled = /^(1|true|yes)$/i.test(process.env.LOOPOVER_UPLOAD_SOURCE ?? "false"); - return { - default: false, - enabled, - source: enabled ? "LOOPOVER_UPLOAD_SOURCE" : "default", - supported: false, - }; -} -// Resolve the current local telemetry opt-in from persisted config. The flag is top-level (not -// per-profile) and defaults to disabled when absent, so an unconfigured install reports opt-out. -function telemetryState(currentConfig = config) { - return { - enabled: currentConfig.telemetryEnabled === true, - default: false, - }; -} -// Report the resolved effective configuration and where each value came from, without leaking -// local absolute paths or token values. Distinct from `status` (health/version), `doctor` -// (diagnostic checks), and `whoami` (session identity): this answers "what config is in effect -// and which source supplied it?". -function configCommand(options) { - const payload = { - apiUrl, - apiUrlSource: resolvedApiUrlSource(), - activeProfile: activeProfileName, - profileCount: profileList(config).length, - configured: existsSync(configPath), - configPathSource: resolvedConfigPathSource(), - cacheDirSource: process.env.LOOPOVER_CACHE_DIR ? "LOOPOVER_CACHE_DIR" : "default", - tokenConfigured: Boolean(getApiToken()), - tokenSource: resolvedTokenSource(), - sourceUpload: sourceUploadState(), - telemetry: telemetryState(), - profile: profilePublicState(activeProfileName), - }; - if (options.json) { - process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); - return; - } - process.stdout.write(`API URL: ${payload.apiUrl} (${payload.apiUrlSource})\n`); - process.stdout.write(`Active profile: ${payload.activeProfile} (${payload.profileCount} configured)\n`); - process.stdout.write(`Config file: ${payload.configured ? "present" : "absent"} (location: ${payload.configPathSource})\n`); - process.stdout.write(`Cache dir: ${payload.cacheDirSource}\n`); - process.stdout.write(`Token: ${payload.tokenConfigured ? `configured (${payload.tokenSource})` : "not configured"}\n`); - process.stdout.write(payload.sourceUpload.enabled - ? `Source upload: enabled via ${payload.sourceUpload.source} (unsupported; unset LOOPOVER_UPLOAD_SOURCE)\n` - : "Source upload: disabled (unsupported)\n"); - process.stdout.write(`Telemetry: ${payload.telemetry.enabled ? "enabled (opt-in)" : "disabled (default)"}\n`); -} -function normalizeProfileName(value) { - const name = String(value ?? defaultProfileName).trim().toLowerCase(); - if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(name)) - throw new Error("Profile names must be 1-64 characters and use letters, numbers, dots, dashes, or underscores."); - return name; -} -function cliOptionValue(args, optionName) { - const dashed = `--${optionName.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; - for (let index = 0; index < args.length; index += 1) { - const value = args[index]; - if (value === dashed) { - const next = args[index + 1]; - return next && !next.startsWith("--") ? next : undefined; - } - if (value?.startsWith(`${dashed}=`)) - return value.slice(dashed.length + 1); - } - return undefined; -} -function upsertProfile(currentConfig, profileName, patch) { - const now = new Date().toISOString(); - const existing = currentConfig.profiles?.[profileName] ?? {}; - const profiles = { - ...(currentConfig.profiles ?? {}), - [profileName]: stripUndefined({ - ...existing, - apiUrl: patch.apiUrl ?? existing.apiUrl, - session: patch.session ?? existing.session, - createdAt: existing.createdAt ?? now, - updatedAt: now, - }), - }; - return normalizeConfig({ ...currentConfig, apiUrl: patch.apiUrl ?? currentConfig.apiUrl, activeProfile: profileName, profiles }); -} -function ensureProfile(currentConfig, profileName, options = {}) { - const existing = currentConfig.profiles?.[profileName]; - const nextConfig = existing ? currentConfig : upsertProfile(currentConfig, profileName, {}); - return options.activate ? setActiveProfile(nextConfig, profileName) : nextConfig; -} -function setActiveProfile(currentConfig, profileName) { - return normalizeConfig({ ...currentConfig, activeProfile: profileName }); -} -function clearProfileSession(currentConfig, profileName) { - const existing = currentConfig.profiles?.[profileName]; - if (!existing) - return currentConfig; - const profiles = { - ...(currentConfig.profiles ?? {}), - [profileName]: stripUndefined({ ...existing, session: undefined, updatedAt: new Date().toISOString() }), - }; - return normalizeConfig({ ...currentConfig, profiles }); -} -function clearAllProfileSessions(currentConfig) { - const profiles = Object.fromEntries(Object.entries(currentConfig.profiles ?? {}).map(([name, profile]) => [name, stripUndefined({ ...profile, session: undefined, updatedAt: new Date().toISOString() })])); - return normalizeConfig({ ...currentConfig, profiles }); -} -function removeProfile(currentConfig, profileName) { - const profiles = { ...(currentConfig.profiles ?? {}) }; - delete profiles[profileName]; - const remaining = Object.keys(profiles); - const activeProfile = currentConfig.activeProfile === profileName ? (profiles[defaultProfileName] ? defaultProfileName : remaining[0] ?? defaultProfileName) : currentConfig.activeProfile; - const session = profileName === defaultProfileName ? undefined : currentConfig.session; - return normalizeConfig({ ...currentConfig, activeProfile, profiles, session }); -} -function setTelemetryEnabled(currentConfig, enabled) { - // normalizeConfig coerces this to a strict boolean and strips it when not exactly `true`, so disabling - // removes the key entirely (default = absent) rather than persisting `telemetryEnabled: false`. - return normalizeConfig({ ...currentConfig, telemetryEnabled: enabled === true ? true : undefined }); -} -function hasPersistedConfigState(currentConfig) { - return Boolean(currentConfig.apiUrl || currentConfig.telemetryEnabled === true || Object.keys(currentConfig.profiles ?? {}).length > 0); -} -function validationFromOptions(options) { - const direct = (options.validation ?? []).map(parseValidationEntry); - const commands = options.validationCommand ?? []; - const statuses = options.validationStatus ?? []; - const summaries = options.validationSummary ?? []; - const durations = options.validationDuration ?? []; - const expanded = commands.map((command, index) => validationEntry({ - command, - statusText: statuses[index], - summaryText: summaries[index], - durationText: durations[index], - })); - return [...direct, ...expanded].filter((entry) => typeof entry.command === "string" && entry.command.length > 0); -} -function parseValidationEntry(entry) { - const parts = String(entry ?? "").split("|").map((part) => part.trim()); - const explicitStatus = normalizeValidationStatus(parts[0]); - const command = explicitStatus ? parts[1] : parts[0]; - const rest = explicitStatus ? parts.slice(2) : parts.slice(1); - const inferredStatusText = !explicitStatus && isValidationStatusLike(rest[0]) ? rest[0] : undefined; - const detailParts = inferredStatusText ? rest.slice(1) : rest; - const durationMs = parseDurationMs(detailParts[0]); - const summaryParts = durationMs !== undefined ? detailParts.slice(1) : detailParts; - return validationEntry({ - command, - statusText: explicitStatus ?? inferredStatusText, - summaryText: summaryParts.join("|"), - durationMs, - }); -} -function validationEntry({ command, statusText, summaryText, durationText, durationMs }) { - const statusSource = nonEmptyString(statusText); - const summarySource = statusSource ? undefined : nonEmptyString(summaryText); - const exitCode = inferValidationExitCode(statusSource, { allowBareCode: true, allowGenericStatus: true }) ?? - inferValidationExitCode(summarySource, { allowBareCode: false, allowGenericStatus: false }); - const status = normalizeValidationStatus(statusSource) ?? - normalizeSummaryValidationStatus(summarySource) ?? - (exitCode !== undefined ? (exitCode === 0 ? "passed" : "failed") : "not_run"); - return stripUndefined({ - command: sanitizeValidationText(command, 160), - status, - summary: sanitizeValidationText(summaryText), - durationMs: durationMs ?? parseDurationMs(durationText), - exitCode, - }); -} -function optionalInteger(value) { - if (value === undefined || value === true) - return undefined; - const parsed = Number(value); - return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined; -} -function parsePositiveIntegerOption(value, flagName) { - if (value === undefined) - return undefined; - const parsed = optionalInteger(value); - if (parsed === undefined || parsed <= 0) - throw new Error(`Pass ${flagName} as a positive integer.`); - return parsed; -} -function normalizeOptionalStringOption(value) { - if (value === undefined) - return undefined; - if (value === true) - return ""; - if (typeof value === "string") - return value; - throw new Error("Expected a string flag value."); -} -function optionalNumber(value) { - if (value === undefined || value === true) - return undefined; - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : undefined; -} -function isValidationStatus(value) { - return Boolean(normalizeValidationStatus(value)); -} -function normalizeValidationStatus(value) { - const text = String(value ?? "").trim().toLowerCase().replace(/[-\s]+/g, "_"); - if (["passed", "pass", "success", "ok", "exit_0", "0"].includes(text)) - return "passed"; - if (["failed", "fail", "failure", "error", "nonzero", "non_zero"].includes(text) || /^exit_[1-9]\d*$/.test(text) || /^[1-9]\d*$/.test(text)) - return "failed"; - if (["not_run", "notrun", "not_ran", "pending"].includes(text)) - return "not_run"; - if (["skipped", "skip"].includes(text)) - return "skipped"; - if (["focused", "focus"].includes(text)) - return "focused"; - if (["unknown", "unclear"].includes(text)) - return "unknown"; - return undefined; -} -function isValidationStatusLike(value) { - return Boolean(normalizeValidationStatus(value) ?? - inferValidationExitCode(value, { allowBareCode: true, allowGenericStatus: true })); -} -function inferValidationExitCode(value, options = {}) { - const text = String(value ?? "").trim().toLowerCase(); - const allowBareCode = options.allowBareCode === true; - const allowGenericStatus = options.allowGenericStatus === true; - if (allowBareCode && /^\d{1,3}$/.test(text)) - return Number(text); - const processExitPattern = /\b(?:exit(?:ed)?(?:\s+(?:code|status))?|exitcode|process\s+(?:exit(?:ed)?|status|code)|command\s+(?:exit(?:ed)?|status|code)|shell\s+(?:exit(?:ed)?|status|code))[\s:_-]*(\d{1,3})\b/; - const genericStatusPattern = /^(?:status|code)[\s:_-]*(\d{1,3})\b/; - const match = text.match(processExitPattern) ?? (allowGenericStatus ? text.match(genericStatusPattern) : null); - if (match) - return Number(match[1]); - if (!allowBareCode && /^\d{1,3}$/.test(text)) - return undefined; - const status = normalizeValidationStatus(text); - if (status === "passed" || status === "focused") - return 0; - if (status === "failed") - return 1; - return undefined; -} -function normalizeSummaryValidationStatus(value) { - const text = nonEmptyString(value); - if (!text || /^\d{1,3}$/.test(text)) - return undefined; - return normalizeValidationStatus(text); -} -function nonEmptyString(value) { - const text = String(value ?? "").trim(); - return text ? text : undefined; -} -function parseDurationMs(value) { - const text = String(value ?? "").trim().toLowerCase(); - const match = text.match(/^(\d+(?:\.\d+)?)\s*(ms|s|sec|secs|m|min|mins)?$/); - if (!match) - return undefined; - const amount = Number(match[1]); - if (!Number.isFinite(amount)) - return undefined; - const unit = match[2] ?? "ms"; - const multiplier = unit.startsWith("m") && unit !== "ms" ? 60000 : unit.startsWith("s") ? 1000 : 1; - return Math.round(amount * multiplier); -} -function sanitizeValidationText(value, maxLength = 240) { - const text = String(value ?? "").replace(/[\r\n\t]+/g, " ").trim(); - if (!text) - return undefined; - const redacted = redactPrivateValidationMetrics(redactLocalPath(text)); - return redacted.length <= maxLength ? redacted : `${redacted.slice(0, maxLength - 3)}...`; -} -function redactPrivateValidationMetrics(text) { - return text.replace(/\b(?:wallet|hotkey|coldkey|mnemonic|raw[-_\s]?trust|private[-_\s]?reviewability|trust[-_\s]?score)\b(?:\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'`,;)]+))?/gi, "[redacted]"); -} -function clientSnippet(client, command) { - if (client === "codex") - return `[mcp_servers.loopover]\ncommand = ${JSON.stringify(command)}\nargs = ["--stdio"]`; - if (client === "claude" || client === "cursor" || client === "mcp") { - return JSON.stringify({ - mcpServers: { - loopover: { - command, - args: ["--stdio"], - }, - }, - }, null, 2); - } - // VS Code's native MCP support uses a `servers` map with an explicit transport type, not the - // `mcpServers` shape the other JSON hosts use, so it needs its own snippet (see .vscode/mcp.json). - if (client === "vscode") { - return JSON.stringify({ - servers: { - loopover: { - type: "stdio", - command, - args: ["--stdio"], - }, - }, - }, null, 2); - } - throw new Error(`Unsupported client: ${client}. Use codex, claude, cursor, mcp, or vscode.`); -} -async function getDecisionPackWithCache(login) { - try { - const payload = await apiGet(`/v1/contributors/${encodeURIComponent(login)}/decision-pack`); - if (isCacheableDecisionPack(payload, login)) - writeDecisionPackCache(login, payload); - return payload; - } - catch (error) { - if (!isDecisionPackCacheFallbackEligible(error)) - throw error; - const cached = readDecisionPackCache(login); - if (!cached) - throw error; - return staleDecisionPackFromCache(cached, error); - } -} -async function getRepoDecisionWithCache(login, owner, repo) { - const repoFullName = `${owner}/${repo}`; - try { - return await apiGet(`/v1/contributors/${encodeURIComponent(login)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/decision`); - } - catch (error) { - if (!isDecisionPackCacheFallbackEligible(error)) - throw error; - const cached = readDecisionPackCache(login); - if (!cached) - throw error; - return repoDecisionFromCachedPack(cached, repoFullName, error); - } -} -function decisionPackToolSummary(login, payload) { - if (payload?.source === "local_cache") - return `LoopOver decision pack for ${login} (stale local cache).`; - if (payload?.freshness === "stale" || payload?.freshness === "rebuilding") - return `LoopOver decision pack for ${login} (${payload.freshness}).`; - return `LoopOver decision pack for ${login}.`; -} -function repoDecisionToolSummary(login, repoFullName, payload) { - if (payload?.source === "local_cache") - return `LoopOver repo decision for ${login} in ${repoFullName} (stale local cache).`; - return `LoopOver repo decision for ${login} in ${repoFullName}.`; -} -function getOpenPrMonitor(login) { - return apiGet(`/v1/contributors/${encodeURIComponent(login)}/open-pr-monitor`); -} -function getPrOutcomes(login, limit) { - const query = new URLSearchParams(); - if (limit != null) - query.set("limit", String(limit)); - const suffix = query.size > 0 ? `?${query}` : ""; - return apiGet(`/v1/contributors/${encodeURIComponent(login)}/pr-outcomes${suffix}`); -} -// #6745: contributor notification feed + mark-read. `postMarkNotificationsRead` sends no ids to mark all -// delivered notifications read, mirroring markNotificationsReadShape's optional ids. -function getNotifications(login) { - return apiGet(`/v1/contributors/${encodeURIComponent(login)}/notifications`); -} -function postMarkNotificationsRead(login, ids) { - return apiPost(`/v1/contributors/${encodeURIComponent(login)}/notifications/read`, ids ? { ids } : {}); -} -// Mirror the API's own `summary` when it sends one, so the CLI and the loopover_monitor_open_prs MCP -// tool (which returns monitor.summary verbatim) never drift into two different sentences for one payload. -function openPrMonitorToolSummary(login, payload) { - const summary = typeof payload?.summary === "string" ? payload.summary.trim() : ""; - if (summary) - return summary; - return `LoopOver open-PR monitor for ${login}.`; -} -function prOutcomesToolSummary(login, payload) { - const summary = typeof payload?.summary === "string" ? payload.summary.trim() : ""; - if (summary) - return summary; - return `LoopOver post-merge outcomes for ${login}.`; -} -function isCacheableDecisionPack(payload, login) { - return payload?.status === "ready" && typeof payload.login === "string" && payload.login.toLowerCase() === login.toLowerCase(); -} -function decisionPackAuthCacheKey() { - const token = getApiToken(); - if (!token) - return null; - return createHash("sha256").update(token).digest("base64url"); -} -function decisionPackCachePath(login, authCacheKey = decisionPackAuthCacheKey()) { - if (!authCacheKey) - return null; - const key = Buffer.from(`${apiUrl}\0${currentApiVersion}\0${login.toLowerCase()}\0${authCacheKey}`).toString("base64url"); - return join(decisionPackCacheDir, `${key}.json`); -} -function writeDecisionPackCache(login, payload) { - const authCacheKey = decisionPackAuthCacheKey(); - if (!authCacheKey) - return { status: "skipped", reason: "missing_auth" }; - const cachedAt = new Date().toISOString(); - const sanitizedPayload = sanitizeDecisionPackForCache(payload); - const entry = { - schemaVersion: decisionPackCacheSchemaVersion, - apiVersion: typeof payload.apiVersion === "string" ? payload.apiVersion : currentApiVersion, - packageVersion, - apiUrl, - authCacheKey, - login: login.toLowerCase(), - cachedAt, - payload: sanitizedPayload, - }; - if (entry.apiVersion !== currentApiVersion) - return { status: "skipped", reason: "api_version_mismatch" }; - const serialized = `${JSON.stringify(entry, null, 2)}\n`; - if (Buffer.byteLength(serialized, "utf8") > decisionPackCacheMaxBytes) - return { status: "skipped", reason: "too_large" }; - mkdirSync(decisionPackCacheDir, { recursive: true, mode: 0o700 }); - const path = decisionPackCachePath(login, authCacheKey); - if (!path) - return { status: "skipped", reason: "missing_auth" }; - writeFileSync(path, serialized, { mode: 0o600 }); - pruneDecisionPackCache(); - return { status: "stored", cachedAt }; -} -function readDecisionPackCache(login) { - const authCacheKey = decisionPackAuthCacheKey(); - const path = decisionPackCachePath(login, authCacheKey); - if (!path || !existsSync(path)) - return null; - try { - const entry = JSON.parse(readFileSync(path, "utf8")); - if (!isCompatibleDecisionPackCacheEntry(entry, login, authCacheKey)) - return null; - return entry; - } - catch { - return null; - } -} -function isCompatibleDecisionPackCacheEntry(entry, login, authCacheKey = decisionPackAuthCacheKey()) { - return (entry && - typeof entry === "object" && - entry.schemaVersion === decisionPackCacheSchemaVersion && - entry.apiVersion === currentApiVersion && - entry.apiUrl === apiUrl && - typeof entry.authCacheKey === "string" && - entry.authCacheKey === authCacheKey && - typeof entry.cachedAt === "string" && - typeof entry.login === "string" && - entry.login.toLowerCase() === login.toLowerCase() && - isCacheableDecisionPack(entry.payload, login)); -} -function staleDecisionPackFromCache(entry, error) { - const payload = entry.payload; - return stripUndefined({ - ...payload, - source: "local_cache", - stale: true, - freshness: "stale", - rebuildEnqueued: false, - cachedAt: entry.cachedAt, - cache: cacheFallbackMetadata(entry, error), - }); -} -function repoDecisionFromCachedPack(entry, repoFullName, error) { - const pack = staleDecisionPackFromCache(entry, error); - const decision = cachedRepoDecision(pack, repoFullName); - return stripUndefined({ - status: decision ? "ready" : "not_found", - login: pack.login, - repoFullName, - generatedAt: pack.generatedAt, - source: "local_cache", - stale: true, - freshness: "stale", - cachedAt: entry.cachedAt, - decision, - dataQuality: pack.dataQuality, - cache: cacheFallbackMetadata(entry, error), - }); -} -function cachedRepoDecision(pack, repoFullName) { - const key = repoFullName.toLowerCase(); - return pack.repoDecisions?.find((decision) => String(decision?.repoFullName ?? "").toLowerCase() === key) ?? null; -} -function cacheFallbackMetadata(entry, error) { - return { - source: "local_cache", - stale: true, - cachedAt: entry.cachedAt, - apiVersion: entry.apiVersion, - schemaVersion: entry.schemaVersion, - reason: "api_unavailable", - detail: sanitizeDiagnosticText(error instanceof Error ? error.message : "api_unavailable"), - rerunGuidance: "Retry when LoopOver API access is restored; cached guidance may be stale.", - clearCommand: "loopover-mcp cache clear", - }; -} -function isDecisionPackCacheFallbackEligible(error) { - const status = error?.status; - if (typeof status !== "number") - return true; - return status === 429 || status >= 500; -} -function sanitizeDecisionPackForCache(value) { - if (Array.isArray(value)) - return value.map((entry) => sanitizeDecisionPackForCache(entry)); - if (typeof value === "string") - return sanitizeCacheString(value); - if (!value || typeof value !== "object") - return value; - const sanitized = {}; - for (const [entryKey, entryValue] of Object.entries(value)) { - if (isForbiddenCacheKey(entryKey)) - continue; - sanitized[entryKey] = sanitizeDecisionPackForCache(entryValue); - } - return sanitized; -} -function isForbiddenCacheKey(key) { - return /^(?:authorization|token|accessToken|apiToken|githubToken|wallet|hotkey|coldkey|mnemonic|privateKey|private_key|sourceContent|sourceContents|fileContent|fileContents|rawSource|rawSourceContent|content|contents|diff|patch|rawDiff|localPath|absolutePath)$/i.test(key); -} -function sanitizeCacheString(value) { - return redactPrivateValidationMetrics(redactLocalPath(sanitizeDiagnosticText(value))); -} -function decisionPackCacheFiles() { - if (!existsSync(decisionPackCacheDir)) - return []; - return readdirSync(decisionPackCacheDir) - .filter((name) => name.endsWith(".json")) - .map((name) => { - const path = join(decisionPackCacheDir, name); - try { - const stats = statSync(path); - return { path, mtimeMs: stats.mtimeMs, size: stats.size }; - } - catch { - return null; - } - }) - .filter((entry) => entry !== null); -} -function pruneDecisionPackCache() { - const files = decisionPackCacheFiles().sort((left, right) => right.mtimeMs - left.mtimeMs); - for (const file of files.slice(decisionPackCacheMaxEntries)) - rmSync(file.path, { force: true }); -} -function clearDecisionPackCache() { - const removed = decisionPackCacheFiles().length; - rmSync(decisionPackCacheDir, { recursive: true, force: true }); - return { - status: "cleared", - removed, - cache: { - source: "local_cache", - maxEntries: decisionPackCacheMaxEntries, - clearCommand: "loopover-mcp cache clear", - }, - }; -} -function inspectDecisionPackCache() { - const files = decisionPackCacheFiles(); - const bytes = files.reduce((sum, file) => sum + file.size, 0); - return { - status: "ok", - entries: files.length, - bytes, - maxEntries: decisionPackCacheMaxEntries, - schemaVersion: decisionPackCacheSchemaVersion, - apiVersion: currentApiVersion, - clearCommand: "loopover-mcp cache clear", - }; -} -// Per-entry view of the offline decision-pack cache, newest first. Surfaces only safe metadata -// (login, when it was cached, the API/package version, size) — never the auth-cache key (a token -// hash) or the cached payload — so it stays consistent with the cache's local-only redaction. -function listDecisionPackCache() { - const files = decisionPackCacheFiles().sort((left, right) => right.mtimeMs - left.mtimeMs); - const entries = files.map((file) => { - try { - const entry = JSON.parse(readFileSync(file.path, "utf8")); - return { - login: typeof entry.login === "string" ? entry.login : null, - cachedAt: typeof entry.cachedAt === "string" ? entry.cachedAt : null, - apiVersion: typeof entry.apiVersion === "string" ? entry.apiVersion : null, - packageVersion: typeof entry.packageVersion === "string" ? entry.packageVersion : null, - bytes: file.size, - }; - } - catch { - return { login: null, cachedAt: null, apiVersion: null, packageVersion: null, bytes: file.size, corrupt: true }; - } - }); - return { - status: "ok", - count: entries.length, - maxEntries: decisionPackCacheMaxEntries, - clearCommand: "loopover-mcp cache clear", - entries, - }; -} -function findExecutable(name) { - for (const directory of String(process.env.PATH ?? "").split(delimiter).filter(Boolean)) { - const candidate = join(directory, name); - if (existsSync(candidate)) - return candidate; - } - return null; -} -function sanitizeDiagnosticText(value, extraPaths = []) { - return redactKnownLocalPaths(value, { - tokens: [ - process.env.LOOPOVER_API_TOKEN, - process.env.LOOPOVER_MCP_TOKEN, - process.env.LOOPOVER_TOKEN, - config.session?.token, - ...profileSessions(config).map((entry) => entry.session.token), - ], - paths: [configPath, process.env.LOOPOVER_CONFIG_PATH, process.env.LOOPOVER_CONFIG_DIR, process.cwd(), homedir(), ...extraPaths], - }); -} -function loadConfig() { - if (!existsSync(configPath)) - return {}; - try { - return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8"))); - } - catch { - return {}; - } -} -function saveConfig(nextConfig) { - mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 }); - writeFileSync(configPath, `${JSON.stringify(configForPersistence(nextConfig), null, 2)}\n`, { mode: 0o600 }); -} -function normalizeConfig(rawConfig) { - const raw = rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) ? rawConfig : {}; - const profiles = {}; - const rawProfiles = raw.profiles && typeof raw.profiles === "object" && !Array.isArray(raw.profiles) ? raw.profiles : {}; - for (const [rawName, rawProfile] of Object.entries(rawProfiles)) { - try { - const name = normalizeProfileName(rawName); - const profile = normalizeProfile(rawProfile); - if (profile) - profiles[name] = profile; - } - catch { - // Ignore malformed profile names in local config instead of leaking paths or tokens. - } - } - if (raw.session?.token && !profiles[defaultProfileName]) { - profiles[defaultProfileName] = normalizeProfile({ - apiUrl: raw.apiUrl, - session: raw.session, - }); - } - let activeProfile = defaultProfileName; - try { - activeProfile = selectProfileName({ ...raw, profiles }, raw.activeProfile); - } - catch { - activeProfile = defaultProfileName; - } - return stripUndefined({ - ...raw, - activeProfile, - profiles, - session: profiles[defaultProfileName]?.session, - // Opt-in telemetry flag (#6239): only a literal `true` counts as enabled, so a malformed or legacy - // value in the config file falls back to the privacy-preserving default (absent = disabled). - telemetryEnabled: raw.telemetryEnabled === true ? true : undefined, - }); -} -function normalizeProfile(rawProfile) { - const raw = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {}; - const session = normalizeSession(raw.session); - return stripUndefined({ - apiUrl: typeof raw.apiUrl === "string" ? raw.apiUrl.replace(/\/+$/, "") : undefined, - session, - createdAt: typeof raw.createdAt === "string" ? raw.createdAt : undefined, - updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : undefined, - }); -} -function normalizeSession(rawSession) { - const raw = rawSession && typeof rawSession === "object" && !Array.isArray(rawSession) ? rawSession : {}; - if (typeof raw.token !== "string" || raw.token.length === 0) - return undefined; - return stripUndefined({ - token: raw.token, - login: typeof raw.login === "string" ? raw.login : undefined, - expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : undefined, - scopes: Array.isArray(raw.scopes) ? raw.scopes.filter((scope) => typeof scope === "string") : [], - }); -} -function configForPersistence(nextConfig) { - const normalized = normalizeConfig(nextConfig); - return stripUndefined({ - apiUrl: normalized.apiUrl, - activeProfile: normalized.activeProfile, - profiles: normalized.profiles, - session: normalized.profiles?.[defaultProfileName]?.session, - telemetryEnabled: normalized.telemetryEnabled, - }); -} -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -async function apiGet(path) { - return apiFetch(path, { method: "GET" }); -} -async function apiPost(path, body) { - return apiFetch(path, { method: "POST", body: JSON.stringify(body) }); -} -async function apiDelete(path, body) { - return apiFetch(path, { method: "DELETE", body: JSON.stringify(body) }); -} -async function apiFetch(path, init, options = {}) { - const token = options.token ?? getApiToken(); - if (options.auth !== false && !token) { - const error = new Error("Run `loopover-mcp login`, or set LOOPOVER_API_TOKEN, LOOPOVER_MCP_TOKEN, or LOOPOVER_TOKEN before starting the MCP wrapper."); - error.status = 401; - error.code = "missing_auth"; - throw error; - } - const controller = new AbortController(); - const timeoutMs = Number(process.env.LOOPOVER_API_TIMEOUT_MS ?? options.timeoutMs ?? 30000); - const timeout = setTimeout(() => controller.abort(), Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30000); - const response = await fetch(`${apiUrl}${path}`, { - ...init, - signal: init?.signal ?? controller.signal, - headers: { - ...(token && options.auth !== false ? { authorization: `Bearer ${token}` } : {}), - "content-type": "application/json", - accept: "application/json", - "x-loopover-mcp-package": packageName, - "x-loopover-mcp-version": packageVersion, - "x-loopover-mcp-client": "loopover-mcp-cli", - }, - }).finally(() => clearTimeout(timeout)); - const text = await response.text(); - let payload = {}; - if (text) { - try { - payload = JSON.parse(text); - } - catch (error) { - if (response.ok) - throw error; - payload = { error: "non_json_response", body: text.slice(0, 500) }; - } - } - if (!response.ok) { - const retry = response.headers.get("retry-after"); - const error = new Error(`LoopOver API ${response.status}${retry ? ` retry-after=${retry}s` : ""}: ${JSON.stringify(payload).slice(0, 500)}`); - error.status = response.status; - throw error; - } - return payload; -} -async function fetchLatestPackageVersion() { - if (/^(1|true|yes)$/i.test(process.env.LOOPOVER_SKIP_NPM_VERSION_CHECK ?? "false")) - return { status: "skipped" }; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); - const response = await fetch(`${npmRegistryUrl}/@loopover%2fmcp/latest`, { - signal: controller.signal, - headers: { accept: "application/json" }, - }).finally(() => clearTimeout(timeout)); - const payload = await response.json().catch(() => ({})); - if (!response.ok || typeof payload.version !== "string") - throw new Error("npm_latest_version_unavailable"); - return { status: "ok", version: payload.version }; -} -function parseSemver(version) { - const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(version ?? "").trim()); - if (!match) - return null; - return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] ?? null }; -} -// Compares two dot-separated semver prerelease strings per the semver spec: -// numeric identifiers compare numerically, others lexically, numeric < non-numeric, -// and a shorter set of identifiers has lower precedence when all earlier ones match. -// -// Numeric identifiers are compared as decimal strings, not via Number(), which loses precision beyond -// Number.MAX_SAFE_INTEGER (2^53-1): two distinct digit strings past that width can round to the SAME float, -// making Number(leftId) !== Number(rightId) wrongly report them as equal (mirrors the same fix already applied -// to compareMcpSemver's comparePrerelease in src/services/mcp-compatibility.ts, #3049). With no leading zeros -// (semver's own numeric-identifier rule), a longer digit string is the larger number, and equal-length strings -// compare lexicographically. -function comparePrerelease(a, b) { - const left = a.split("."); - const right = b.split("."); - for (let index = 0; index < Math.max(left.length, right.length); index += 1) { - const leftId = left[index]; - const rightId = right[index]; - if (leftId === undefined) - return -1; - if (rightId === undefined) - return 1; - const leftNumeric = /^\d+$/.test(leftId); - const rightNumeric = /^\d+$/.test(rightId); - if (leftNumeric && rightNumeric) { - if (leftId.length !== rightId.length) - return leftId.length < rightId.length ? -1 : 1; - if (leftId !== rightId) - return leftId < rightId ? -1 : 1; - } - else if (leftNumeric !== rightNumeric) { - return leftNumeric ? -1 : 1; - } - else if (leftId !== rightId) { - return leftId < rightId ? -1 : 1; - } - } - return 0; -} -// Returns -1 if a < b, 1 if a > b, 0 if equal, or null when either side is unparseable. -function compareSemver(a, b) { - const left = parseSemver(a); - const right = parseSemver(b); - if (!left || !right) - return null; - for (const part of ["major", "minor", "patch"]) { - if (left[part] !== right[part]) - return left[part] < right[part] ? -1 : 1; - } - if (left.prerelease === right.prerelease) - return 0; - // A release version has higher precedence than any prerelease of the same core. - if (left.prerelease === null) - return 1; - if (right.prerelease === null) - return -1; - return comparePrerelease(left.prerelease, right.prerelease); -} -// Maps a raw npm-latest lookup into a single install state. `comparison` is the result of -// compareSemver(local, latest): negative means local is behind (stale), positive means ahead. -function classifyVersionState(latestStatus, latestVersion, comparison) { - if (latestStatus === "skipped") - return "skipped"; - if (!latestVersion) - return "unavailable"; - if (comparison === null) - return "unknown"; - if (comparison < 0) - return "stale"; - if (comparison > 0) - return "ahead"; - return "current"; -} -// Shared by `status` and `doctor`: compares the local install against npm latest and -// produces deterministic upgrade guidance. Never throws and never returns sensitive data. -async function inspectInstallVersion(apiRecommendedVersion) { - let latest; - try { - latest = await fetchLatestPackageVersion(); - } - catch (error) { - latest = { status: "unavailable", error: sanitizeDiagnosticText(error instanceof Error ? error.message : "npm_version_check_failed") }; - } - if (latest.status === "unavailable" && typeof apiRecommendedVersion === "string" && apiRecommendedVersion.length > 0) { - latest = { status: "api", version: apiRecommendedVersion }; - } - const latestVersion = typeof latest.version === "string" ? latest.version : null; - const comparison = latestVersion ? compareSemver(packageVersion, latestVersion) : null; - const state = classifyVersionState(latest.status, latestVersion, comparison); - const stale = state === "stale"; - return stripUndefined({ - name: packageName, - version: packageVersion, - latestVersion, - latestStatus: latest.status ?? "ok", - state, - updateAvailable: stale, - upgradeCommand: stale ? upgradeCommand : undefined, - npxFallback: stale ? npxFallbackCommand : undefined, - detail: latest.error, - }); -} -async function inspectApiCompatibility(health) { - try { - const report = await apiFetch(compatibilityPath, { method: "GET" }, { auth: false, timeoutMs: 5000 }); - return { report, evaluation: evaluateApiCompatibility(report, "compatibility_endpoint") }; - } - catch (error) { - const report = { - status: "unavailable", - reason: "compatibility_endpoint_unavailable", - error: sanitizeDiagnosticText(error instanceof Error ? error.message : "compatibility_check_failed"), - }; - const fallback = evaluateApiCompatibility(health, "health"); - return { - report, - evaluation: fallback.reason === "not_reported" ? evaluateApiCompatibility(report, "compatibility_endpoint") : fallback, - }; - } -} -// Prefer the first-class compatibility endpoint, but keep supporting older APIs that only -// advertise `minMcpVersion`/`minClientVersion` on /health. -function evaluateApiCompatibility(report, source) { - if (!report || report.status === "unreachable") - return { status: "unavailable", reason: "api_unreachable", source }; - if (report.status === "unavailable") { - return stripUndefined({ status: "unavailable", reason: report.reason ?? "compatibility_unavailable", source, detail: report.error }); - } - const minVersion = compatibilityMinimumVersion(report); - if (!minVersion) - return { status: "unavailable", reason: "not_reported", source }; - const comparison = compareSemver(packageVersion, minVersion); - const latestRecommendedVersion = compatibilityLatestRecommendedVersion(report); - const apiVersion = typeof report.apiVersion === "string" ? report.apiVersion : undefined; - const warnings = Array.isArray(report.compatibilityWarnings) ? report.compatibilityWarnings : Array.isArray(report.warnings) ? report.warnings : []; - const breakingChanges = Array.isArray(report.breakingChanges) ? report.breakingChanges : []; - if (comparison === null) - return stripUndefined({ status: "unknown", source, minVersion, latestRecommendedVersion, apiVersion, warnings, breakingChanges }); - if (comparison < 0) - return stripUndefined({ status: "incompatible", source, minVersion, latestRecommendedVersion, apiVersion, warnings, breakingChanges, upgradeCommand }); - return stripUndefined({ status: "compatible", source, minVersion, latestRecommendedVersion, apiVersion, warnings, breakingChanges }); -} -function compatibilityMinimumVersion(report) { - if (typeof report?.mcp?.minimumSupportedVersion === "string") - return report.mcp.minimumSupportedVersion; - if (typeof report?.minimumSupportedMcpVersion === "string") - return report.minimumSupportedMcpVersion; - if (typeof report?.minMcpVersion === "string") - return report.minMcpVersion; - if (typeof report?.minClientVersion === "string") - return report.minClientVersion; - return null; -} -function compatibilityLatestRecommendedVersion(report) { - if (typeof report?.mcp?.latestRecommendedVersion === "string") - return report.mcp.latestRecommendedVersion; - if (typeof report?.mcp?.latestPackageVersion === "string") - return report.mcp.latestPackageVersion; - if (typeof report?.latestRecommendedMcpVersion === "string") - return report.latestRecommendedMcpVersion; - if (typeof report?.latestPackageVersion === "string") - return report.latestPackageVersion; - return null; -} -async function analyzeCurrentBranch(input) { - const workspace = resolveWorkspaceCwd(input); - const payload = buildBranchAnalysisPayload({ ...input, cwd: workspace.cwd }); - const { localScorerStatus, ...body } = payload; - const analysis = await apiPost("/v1/local/branch-analysis", body); - return { - local: { - sourceUpload: false, - workspaceRoots: { - available: workspace.rootsAvailable, - count: workspace.rootCount, - cwdInsideRoot: workspace.rootsAvailable ? true : undefined, - pathsIncluded: false, - }, - repoFullName: body.repoFullName, - baseRef: body.baseRef, - headRef: body.headRef, - branchName: body.branchName, - baseSha: body.baseSha, - headSha: body.headSha, - mergeBaseSha: body.mergeBaseSha, - remoteTrackingSha: body.remoteTrackingSha, - changedFileCount: body.changedFiles?.length ?? 0, - testFileCount: body.changedFiles?.filter((file) => isTestFile(file.path)).length ?? 0, - passedValidationCount: body.validation?.filter((entry) => entry.status === "passed").length ?? 0, - localScorerStatus: sanitizeLocalScorerStatus(localScorerStatus), - setupGuidance: setupGuidanceForLocalScorer(localScorerStatus), - }, - analysis, - }; -} -async function agentPreparePrPacket(input) { - const workspace = resolveWorkspaceCwd(input); - const payload = buildBranchAnalysisPayload({ ...input, cwd: workspace.cwd }); - const { localScorerStatus: _localScorerStatus, ...body } = payload; - return apiPost("/v1/agent/prepare-pr-packet", body); -} -// #1968 review-pr: a thin composition of the existing preflight + slop-risk + lint-pr-text checks -// into one report, so a contributor's own local agent can see everything the gate would flag before -// ever opening a PR. Reuses analyzeCurrentBranch (preflight) and collectLocalDiff (the same diff -// metadata previewLocalScore already sends) rather than reimplementing any check. Each sub-check is -// isolated with its own try/catch: one flaky endpoint degrades that section to a `failed` status with -// a public-safe reason instead of hiding the sections that did succeed. -async function reviewLocalPr(input) { - const result = await analyzeCurrentBranch(input); - const workspace = resolveWorkspaceCwd(input); - const diff = collectLocalDiff(workspace.cwd, input.baseRef, input.workspaceRoots); - const commitMessages = input.commitMessages?.length ? input.commitMessages : undefined; - const prBody = input.body; - const linkedIssue = input.linkedIssues?.[0]; - const slopRisk = await runReviewCheck(() => apiPost("/v1/lint/slop-risk", { - changedFiles: diff.changedFiles.map((path) => ({ path })), - description: prBody, - testFiles: diff.testFiles, - })); - const prTextLint = await runReviewCheck(() => apiPost("/v1/lint/pr-text", { - ...(commitMessages ? { commitMessages } : {}), - ...(prBody !== undefined ? { prBody } : {}), - ...(linkedIssue !== undefined ? { linkedIssue } : {}), - })); - const sections = [ - { name: "preflight", status: preflightSectionStatus(result.analysis.preflight?.status) }, - { name: "slop_risk", status: slopRisk.ok ? slopRiskSectionStatus(slopRisk.value) : "fail" }, - { name: "pr_text_lint", status: prTextLint.ok ? prTextLintSectionStatus(prTextLint.value) : "fail" }, - ]; - return { - local: result.local, - preflight: result.analysis.preflight, - prPacket: result.analysis.prPacket, - workspaceIntelligence: publicSafeWorkspaceIntelligence(result.analysis.workspaceIntelligence), - slopRisk: slopRisk.ok ? slopRisk.value : undefined, - slopRiskError: slopRisk.ok ? undefined : slopRisk.reason, - prTextLint: prTextLint.ok ? prTextLint.value : undefined, - prTextLintError: prTextLint.ok ? undefined : prTextLint.reason, - overallStatus: reviewOverallStatus(sections), - sections, - }; -} -async function runReviewCheck(run) { - try { - return { ok: true, value: await run() }; - } - catch (error) { - return { ok: false, reason: error instanceof Error ? error.message : "review_check_failed" }; - } -} -function preflightSectionStatus(status) { - if (status === "hold") - return "fail"; - if (status === "needs_work") - return "warn"; - return "pass"; -} -function slopRiskSectionStatus(value) { - if (value?.band === "high" || value?.band === "elevated") - return "warn"; - return "pass"; -} -function prTextLintSectionStatus(value) { - if (value?.verdict === "weak") - return "warn"; - return "pass"; -} -function reviewOverallStatus(sections) { - if (sections.some((section) => section.status === "fail")) - return "fail"; - if (sections.some((section) => section.status === "warn")) - return "warn"; - return "pass"; -} -async function previewLocalScore(input) { - const workspace = resolveWorkspaceCwd(input); - const cwd = workspace.cwd; - const diff = collectLocalDiff(cwd, input.baseRef, input.workspaceRoots); - const branchPayload = buildBranchAnalysisPayload({ ...input, login: input.contributorLogin ?? "local", cwd, repoFullName: input.repoFullName, baseRef: input.baseRef }); - const upstreamPreview = branchPayload.localScorerStatus; - const estimatedSourceLines = input.sourceLines ?? Math.max(1, diff.changedLineCount - diff.testFiles.length); - const body = { - repoFullName: input.repoFullName, - targetType: "local_diff", - targetKey: input.targetKey ?? localDiffTargetKey(branchPayload, input.baseRef), - contributorLogin: input.contributorLogin, - labels: input.labels, - linkedIssueMode: input.linkedIssueMode, - sourceTokenScore: input.sourceTokenScore ?? estimatedSourceLines, - sourceLines: estimatedSourceLines, - totalTokenScore: input.totalTokenScore ?? diff.changedLineCount, - testTokenScore: diff.testFiles.length, - openPrCount: input.openPrCount, - credibility: input.credibility, - changesRequestedCount: input.changesRequestedCount, - pendingMergedPrCount: input.pendingMergedPrCount, - pendingClosedPrCount: input.pendingClosedPrCount, - approvedPrCount: input.approvedPrCount, - expectedOpenPrCountAfterMerge: input.expectedOpenPrCountAfterMerge, - projectedCredibility: input.projectedCredibility, - scenarioNotes: input.scenarioNotes, - branchEligibility: input.branchEligibility, - metadataOnly: !upstreamPreview.ok, - }; - return { - localDiff: { - changedFiles: diff.changedFiles, - changedLineCount: diff.changedLineCount, - testFiles: diff.testFiles, - codeFiles: diff.codeFiles, - commitMessage: input.commitMessage ?? diff.commitMessage, - }, - upstreamPreview: sanitizeLocalScorerStatus(upstreamPreview), - remotePreview: await apiPost("/v1/scoring/preview", body), - setupGuidance: upstreamPreview.ok - ? [] - : setupGuidanceForLocalScorer(upstreamPreview), - }; -} -function localDiffTargetKey(branchPayload, baseRef) { - return [ - branchPayload.repoFullName, - branchPayload.branchName ?? branchPayload.headRef ?? "local", - branchPayload.headSha ?? baseRef ?? "diff", - ] - .filter(Boolean) - .join(":"); -} -function branchEligibilityFromOptions(options) { - const status = options.branchEligibility ?? options.branchEligibilityStatus; - if (!["eligible", "ineligible", "unknown"].includes(status)) - return undefined; - const source = ["github_metadata", "local_metadata", "registry", "user_supplied"].includes(options.branchEligibilitySource) ? options.branchEligibilitySource : "user_supplied"; - return stripUndefined({ - status, - source, - reason: options.branchEligibilityReason, - checkedAt: options.branchEligibilityCheckedAt, - stale: optionalBoolean(options.branchEligibilityStale), - }); -} -function optionalBoolean(value) { - if (value === undefined) - return undefined; - if (value === true) - return true; - if (typeof value === "string") { - const normalized = value.trim().toLowerCase(); - if (["false", "0", "no", "off"].includes(normalized)) - return false; - if (["true", "1", "yes", "on"].includes(normalized)) - return true; - } - return Boolean(value); -} -function toolResult(summary, data) { - return { - content: [ - { - type: "text", - text: `${summary}\n\n${JSON.stringify(data, null, 2)}`, - }, - ], - structuredContent: data, - }; -} -function camel(value) { - return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase()); -} -function stripUndefined(value) { - if (Array.isArray(value)) - return value.map(stripUndefined); - if (!value || typeof value !== "object") - return value; - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).map(([key, entry]) => [key, stripUndefined(entry)])); -} diff --git a/packages/loopover-mcp/lib/cli-error.js b/packages/loopover-mcp/lib/cli-error.js deleted file mode 100644 index f46d466701..0000000000 --- a/packages/loopover-mcp/lib/cli-error.js +++ /dev/null @@ -1,20 +0,0 @@ -/** Shared CLI failure output (#5928): when `--json` is set, emit a parseable `{ ok: false, error }` object on - * stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */ -export function reportCliFailure(wantsJson, message, exitCode = 2) { - if (wantsJson) { - console.log(JSON.stringify({ ok: false, error: message }, null, 2)); - } - else { - console.error(message); - } - return exitCode; -} -/** True when argv includes `--json` or `--json=...` (used before a full parse result exists). */ -export function argsWantJson(args) { - return args.some((arg) => arg === "--json" || arg?.startsWith("--json=")); -} -/** Normalize a thrown value to a safe error string for CLI output. */ -export function describeCliError(error) { - return error instanceof Error ? error.message : String(error); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2xpLWVycm9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO3FHQUNxRztBQUVyRyxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsU0FBa0IsRUFBRSxPQUFlLEVBQUUsUUFBUSxHQUFHLENBQUM7SUFDaEYsSUFBSSxTQUFTLEVBQUUsQ0FBQztRQUNkLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3RFLENBQUM7U0FBTSxDQUFDO1FBQ04sT0FBTyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6QixDQUFDO0lBQ0QsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQztBQUVELGlHQUFpRztBQUNqRyxNQUFNLFVBQVUsWUFBWSxDQUFDLElBQXVCO0lBQ2xELE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRyxLQUFLLFFBQVEsSUFBSSxHQUFHLEVBQUUsVUFBVSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUM7QUFDNUUsQ0FBQztBQUVELHNFQUFzRTtBQUN0RSxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsS0FBYztJQUM3QyxPQUFPLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNoRSxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-mcp/lib/format-table.js b/packages/loopover-mcp/lib/format-table.js deleted file mode 100644 index def5925db4..0000000000 --- a/packages/loopover-mcp/lib/format-table.js +++ /dev/null @@ -1,55 +0,0 @@ -// Pure, dependency-free monospace table renderer shared by the stdio CLI's report-shaped commands -// (#2231). Kept in lib/ (not the bin) so it can be unit-tested in isolation: the bin auto-runs its -// CLI/MCP entrypoint on import, so importable helpers live here instead. -// Normalize either an array of row objects or an explicit { headers, rows } shape into a common -// { headers, rows } form. For an array of objects the column set is the union of keys in first-seen -// order, and each key doubles as its own header label. -function normalizeInput(input) { - if (Array.isArray(input)) { - const keys = []; - for (const row of input) { - for (const key of Object.keys(row ?? {})) - if (!keys.includes(key)) - keys.push(key); - } - return { headers: keys.map((key) => ({ key, label: key })), rows: input }; - } - const headers = (input?.headers ?? []).map((header) => typeof header === "string" ? { key: header, label: header } : { key: header.key, label: header.label ?? header.key, align: header.align }); - return { headers, rows: input?.rows ?? [] }; -} -function stringifyCell(value) { - return value === undefined || value === null ? "" : String(value); -} -// A row is either an object keyed by column key or a positional array; read the matching cell. -function readCell(row, header, columnIndex) { - if (Array.isArray(row)) - return row[columnIndex]; - return row?.[header.key]; -} -function resolveAlign(header, opts) { - const fromOpts = opts.align && (opts.align[header.key] ?? opts.align[header.label]); - return header.align ?? fromOpts ?? "left"; -} -/** - * Render tabular data as an aligned, monospace plain-text table (header row + one line per row). - * Accepts an array of row objects, or `{ headers, rows }` with string/`{ key, label, align }` - * headers and object/array rows. `opts.align` maps a column key/label to `"left"`|`"right"`; - * `opts.gap` sets the space count between columns (default 2). Pure — no I/O, no dependencies. - * Returns "" when there are no columns. - */ -export function formatTable(input, opts = {}) { - const { headers, rows } = normalizeInput(input); - if (headers.length === 0) - return ""; - const gap = " ".repeat(Math.max(1, opts.gap ?? 2)); - const aligns = headers.map((header) => resolveAlign(header, opts)); - // Precompute every cell's text so column widths and the rendered rows read the same strings. - const bodyCells = rows.map((row) => headers.map((header, column) => stringifyCell(readCell(row, header, column)))); - // Every `cells`/`widths` array here has exactly `headers.length` entries (built via headers.map), so - // indexing by a column index drawn from that same range is always in bounds. - const widths = headers.map((header, column) => Math.max(header.label.length, ...bodyCells.map((cells) => cells[column].length), 0)); - // Trim trailing padding so a left-aligned final column never emits dangling spaces. - const renderRow = (cells) => cells.map((text, column) => (aligns[column] === "right" ? text.padStart(widths[column]) : text.padEnd(widths[column]))).join(gap).replace(/\s+$/, ""); - return [renderRow(headers.map((header) => header.label)), ...bodyCells.map(renderRow)].join("\n"); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybWF0LXRhYmxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZm9ybWF0LXRhYmxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGtHQUFrRztBQUNsRyxtR0FBbUc7QUFDbkcseUVBQXlFO0FBV3pFLGdHQUFnRztBQUNoRyxvR0FBb0c7QUFDcEcsdURBQXVEO0FBQ3ZELFNBQVMsY0FBYyxDQUFDLEtBQW9DO0lBQzFELElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3pCLE1BQU0sSUFBSSxHQUFhLEVBQUUsQ0FBQztRQUMxQixLQUFLLE1BQU0sR0FBRyxJQUFJLEtBQUssRUFBRSxDQUFDO1lBQ3hCLEtBQUssTUFBTSxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksRUFBRSxDQUFDO2dCQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQztvQkFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3BGLENBQUM7UUFDRCxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxHQUFHLEVBQUUsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDNUUsQ0FBQztJQUNELE1BQU0sT0FBTyxHQUFHLENBQUMsS0FBSyxFQUFFLE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUNwRCxPQUFPLE1BQU0sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxFQUFFLE1BQU0sQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLE1BQU0s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 \ No newline at end of file diff --git a/packages/loopover-mcp/lib/local-branch.js b/packages/loopover-mcp/lib/local-branch.js deleted file mode 100644 index 2342a8a25d..0000000000 --- a/packages/loopover-mcp/lib/local-branch.js +++ /dev/null @@ -1,638 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { realpathSync } from "node:fs"; -import { isAbsolute, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { isCodeFile, isTestPath as isTestFile } from "@loopover/engine/signals/test-evidence"; -import { redactLocalPath } from "./redact-local-path.js"; -export { isCodeFile, isTestFile }; -export { redactLocalPath }; -function stripTrailingSlashes(value) { - let end = value.length; - while (end > 0 && value.charCodeAt(end - 1) === 47) - end -= 1; - return end === value.length ? value : value.slice(0, end); -} -export function parseGitRemote(remoteUrl) { - const trimmed = stripTrailingSlashes(String(remoteUrl ?? "").trim()); - const patterns = [ - /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/, - /^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/, - /^ssh:\/\/git@github\.com\/([^/]+)\/(.+?)(?:\.git)?$/, - ]; - for (const pattern of patterns) { - const match = trimmed.match(pattern); - if (match?.[1] && match[2]) - return `${match[1]}/${match[2].replace(/\.git$/, "")}`; - } - return undefined; -} -export function collectLocalDiff(cwd, baseRef, workspaceRoots) { - const metadata = collectLocalBranchMetadata({ cwd, baseRef, login: "local", workspaceRoots }); - return { - // metadata.title is provably defined here: this call always resolves branchName through the same - // "??...??\"local-branch\"" chain collectLocalBranchMetadata itself uses (collectLocalDiff has no - // way to override branchName to force it empty), and titleFromBranch("local-branch") is non-empty. - title: metadata.title, - commitMessage: metadata.commitMessages.join("\n\n").trim(), - changedFiles: metadata.changedFiles.map((file) => file.path), - // file.additions/deletions are always real numbers here (ChangedFile's own type, always populated by - // collectChangedFiles) -- collectLocalDiff has no way to inject a differently-shaped changedFiles. - changedLineCount: metadata.changedFiles.reduce((sum, file) => sum + file.additions + file.deletions, 0), - testFiles: metadata.changedFiles.map((file) => file.path).filter(isTestFile), - codeFiles: metadata.changedFiles.map((file) => file.path).filter(isCodeFile), - }; -} -export function collectLocalBranchMetadata(input) { - assertSourceUploadDisabled(); - const workspace = resolveWorkspaceCwd(input); - const cwd = workspace.cwd; - const baseRef = input.baseRef ?? defaultBaseRef(cwd); - const remoteUrl = gitLines(cwd, ["config", "--get", "remote.origin.url"])[0] ?? ""; - const repoFullName = input.repoFullName ?? parseGitRemote(remoteUrl); - if (!repoFullName) - throw new Error("Could not infer repoFullName from git remote; pass --repo owner/repo."); - const branchName = input.branchName ?? gitLines(cwd, ["branch", "--show-current"])[0] ?? "local-branch"; - const headRef = input.headRef ?? gitLines(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])[0] ?? branchName; - const baseSha = gitLines(cwd, ["rev-parse", "--verify", baseRef])[0]; - const headSha = gitLines(cwd, ["rev-parse", "--verify", "HEAD"])[0]; - const mergeBaseSha = gitLines(cwd, ["merge-base", baseRef, "HEAD"])[0]; - const remoteTrackingSha = collectRemoteTrackingSha(cwd, baseRef); - const changedFiles = collectChangedFiles(cwd, baseRef); - const pendingCommitCount = input.pendingCommitCount ?? collectPendingCommitCount(cwd, baseRef); - const ciStatusHints = input.ciStatusHints ?? collectCiStatusHints(cwd, baseRef, changedFiles); - const commitMessages = input.commitMessages ?? collectCommitMessages(cwd, baseRef); - const title = input.title ?? titleFromBranch(branchName) ?? firstCommitTitle(commitMessages); - const linkedIssues = [...new Set([...(input.linkedIssues ?? []), ...extractLinkedIssues([branchName, title, input.body, ...commitMessages].filter(Boolean).join("\n"))])].sort((left, right) => left - right); - const payload = { - login: input.login, - repoFullName, - baseRef, - headRef, - branchName, - baseSha, - headSha, - mergeBaseSha, - remoteTrackingSha, - commitMessages, - changedFiles, - validation: input.validation, - linkedIssues, - labels: input.labels, - title, - body: input.body, - pendingMergedPrCount: input.pendingMergedPrCount, - pendingClosedPrCount: input.pendingClosedPrCount, - approvedPrCount: input.approvedPrCount, - expectedOpenPrCountAfterMerge: input.expectedOpenPrCountAfterMerge, - projectedCredibility: input.projectedCredibility, - scenarioNotes: input.scenarioNotes, - pendingCommitCount, - ciStatusHints, - branchEligibility: input.branchEligibility, - }; - return stripUndefined(payload); -} -export function collectPendingCommitCount(cwd, baseRef) { - const count = gitLines(cwd, ["rev-list", "--count", `${baseRef}..HEAD`])[0]; - const parsed = Number(count); - return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : 0; -} -export function collectCiStatusHints(cwd, baseRef, changedFiles = []) { - const hints = []; - const paths = changedFiles.map((file) => file.path).filter(Boolean); - if (paths.some((path) => /^\.github\/workflows\//i.test(path))) { - hints.push("Workflow files changed; CI required-check behavior may change after merge."); - } - if (paths.some((path) => /(^|\/)(Makefile|Dockerfile|package\.json|pyproject\.toml|go\.mod|Cargo\.toml)$/i.test(path))) { - hints.push("Build or dependency manifests changed; rerun the repo's standard validation commands."); - } - const pendingCommits = collectPendingCommitCount(cwd, baseRef); - if (pendingCommits > 0) { - hints.push(`${pendingCommits} local commit(s) ahead of ${baseRef}; push or rebase before reviewers rely on the latest diff.`); - } - return hints; -} -export function buildBranchAnalysisPayload(input) { - const workspace = resolveWorkspaceCwd(input); - const metadata = collectLocalBranchMetadata({ ...input, cwd: workspace.cwd }); - const scorerMetadata = { ...metadata, repoRoot: workspace.cwd }; - const scorerCommand = resolveScorePreviewCommand(input); - const externalPreview = runExternalScorePreview(scorerMetadata, scorerCommand); - // externalPreview.ok true is only ever set (in runExternalScorePreview) alongside a validated, - // non-null, non-array object payload -- ScorerStatus's own `payload?: unknown` just can't express - // that correlation as a discriminated union without a much heavier refactor of this shared type. - const localScorer = externalPreview.ok - ? normalizeScorerOutput(externalPreview.payload) - : metadataOnlyScorer(externalPreview); - return { - ...metadata, - localScorer, - localScorerStatus: sanitizeLocalScorerStatus(externalPreview), - }; -} -export function resolveWorkspaceCwd(input = {}) { - const workspaceRoots = normalizeMcpWorkspaceRoots(input.workspaceRoots); - if (workspaceRoots.length === 0) { - return { - cwd: safeResolvedPath(input.cwd ?? process.cwd()), - rootsAvailable: false, - rootCount: 0, - }; - } - const selectedRoot = workspaceRoots[0]; - const requestedCwd = input.cwd === undefined || input.cwd === null || input.cwd === "" - ? selectedRoot.path - : isAbsolute(String(input.cwd)) - ? String(input.cwd) - : resolve(selectedRoot.path, String(input.cwd)); - const cwd = safeResolvedPath(requestedCwd); - const containingRoot = workspaceRoots.find((root) => pathIsInside(cwd, root.path)); - if (!containingRoot) { - throw new Error("Selected workspace is outside the MCP roots exposed by the client."); - } - return { - cwd, - rootsAvailable: true, - rootCount: workspaceRoots.length, - }; -} -export function normalizeMcpWorkspaceRoots(roots) { - if (!Array.isArray(roots)) - return []; - const normalized = []; - const seen = new Set(); - for (const root of roots) { - const uri = typeof root?.uri === "string" ? root.uri : ""; - if (!uri.startsWith("file:")) - continue; - try { - const path = safeResolvedPath(fileURLToPath(uri)); - if (seen.has(path)) - continue; - seen.add(path); - normalized.push({ path }); - } - catch { - // Ignore non-local or malformed root URIs. Clients without usable roots fall back to cwd. - } - } - return normalized; -} -function safeResolvedPath(path) { - const resolved = resolve(String(path)); - try { - return realpathSync(resolved); - } - catch { - return resolved; - } -} -function pathIsInside(candidate, root) { - const child = safeResolvedPath(candidate); - const parent = safeResolvedPath(root); - const childRelativeToParent = relative(parent, child); - return childRelativeToParent === "" || (!!childRelativeToParent && !childRelativeToParent.startsWith("..") && !isAbsolute(childRelativeToParent)); -} -export function resolveScorePreviewCommand(input = {}) { - const explicit = input.scorePreviewCommand ?? process.env.GITTENSOR_SCORE_PREVIEW_CMD; - if (typeof explicit === "string" && explicit.trim()) - return explicit.trim(); - return undefined; -} -export function referenceScorePreviewExample(kind = "metadata") { - const script = kind === "gittensor" ? "gittensor-score-preview.py" : "gittensor-score-preview.mjs"; - const interpreter = kind === "gittensor" ? "python3" : "node"; - return `${interpreter} ./node_modules/@loopover/mcp/scripts/${script}`; -} -export function redactScorerCommand(command) { - const text = String(command ?? "").trim(); - if (!text) - return text; - const parts = splitCommand(text); - const interpreter = parts[0]?.split(/[\\/]/).pop() ?? "command"; - const script = parts.at(-1)?.split(/[\\/]/).pop(); - if (script && /\.(mjs|js|cjs|py)$/i.test(script)) - return `${interpreter} /${script}`; - return ""; -} -export function sanitizeLocalScorerStatus(status) { - if (!status || typeof status !== "object") - return status; - return stripUndefined({ - ...status, - reason: status.reason ? redactLocalPath(String(status.reason)) : undefined, - stderr: status.stderr ? redactLocalPath(String(status.stderr)) : undefined, - scorerCommand: status.scorerCommand ? redactScorerCommand(status.scorerCommand) : undefined, - }); -} -export function runExternalScorePreview(metadata, scorerCommand) { - const timeoutMs = scorePreviewTimeoutMs(); - if (!scorerCommand) { - return scorerFailure("missing_scorer_command", "GITTENSOR_SCORE_PREVIEW_CMD is not configured."); - } - const parts = splitCommand(scorerCommand); - const command = parts[0]; - const args = parts.slice(1); - if (!command) { - return scorerFailure("empty_scorer_command", "GITTENSOR_SCORE_PREVIEW_CMD is empty."); - } - const startedAt = Date.now(); - try { - const output = execFileSync(command, args, { - input: JSON.stringify({ - ...metadata, - repoRoot: metadata.repoRoot ?? metadata.cwd, - gittensorRoot: process.env.GITTENSOR_ROOT, - }), - encoding: "utf8", - timeout: timeoutMs, - stdio: ["pipe", "pipe", "pipe"], - }); - const durationMs = Date.now() - startedAt; - let payload; - try { - payload = JSON.parse(output); - } - catch { - return scorerFailure("malformed_json", "External scorer stdout was not valid JSON.", { - durationMs, - stderr: truncateText(output), - fallbackMode: "metadata_only", - }); - } - if (!payload || typeof payload !== "object" || Array.isArray(payload)) { - return scorerFailure("malformed_json", "External scorer stdout must be a JSON object.", { - durationMs, - fallbackMode: "metadata_only", - }); - } - const normalized = normalizeScorerOutput(payload); - if (normalized.sourceTokenScore === undefined && normalized.totalTokenScore === undefined) { - return scorerFailure("malformed_json", "External scorer JSON must include sourceTokenScore or totalTokenScore.", { - durationMs, - fallbackMode: "metadata_only", - }); - } - return stripUndefined({ - ok: true, - code: "success", - reason: "external_scorer_succeeded", - durationMs, - payload, - fallbackMode: "external_command", - }); - } - catch (error) { - return classifyScorerExecFailure(error, Date.now() - startedAt, scorerCommand); - } -} -export function setupGuidanceForLocalScorer(status) { - if (status.ok) - return []; - const safeStatus = sanitizeLocalScorerStatus(status); - const code = safeStatus.code ?? inferScorerCode(safeStatus.reason); - const guidance = [ - "LoopOver used metadata-only analysis because no external scorer succeeded.", - ]; - switch (code) { - case "missing_scorer_command": - guidance.push(`Set GITTENSOR_SCORE_PREVIEW_CMD, for example: export GITTENSOR_SCORE_PREVIEW_CMD="${referenceScorePreviewExample("metadata")}"`); - guidance.push(`For tree-sitter scoring with a local gittensor checkout: export GITTENSOR_ROOT= && export GITTENSOR_SCORE_PREVIEW_CMD="${referenceScorePreviewExample("gittensor")}"`); - break; - case "empty_scorer_command": - guidance.push("GITTENSOR_SCORE_PREVIEW_CMD is set but empty; provide a command that reads branch metadata JSON from stdin."); - break; - case "timeout": - guidance.push(`External scorer exceeded ${scorePreviewTimeoutMs()}ms; simplify the scorer or raise GITTENSOR_SCORE_PREVIEW_TIMEOUT_MS.`); - break; - case "malformed_json": - guidance.push("External scorer must print one JSON object with sourceTokenScore/totalTokenScore fields to stdout."); - if (safeStatus.stderr) - guidance.push(`Last scorer stdout snippet: ${truncateText(safeStatus.stderr, 160)}`); - break; - case "non_zero_exit": - guidance.push("External scorer exited with a non-zero status; inspect stderr and run loopover-mcp doctor."); - if (safeStatus.stderr) - guidance.push(`Scorer stderr: ${truncateText(safeStatus.stderr, 160)}`); - if (typeof safeStatus.exitCode === "number") - guidance.push(`Exit code: ${safeStatus.exitCode}`); - break; - default: - guidance.push("Set GITTENSOR_SCORE_PREVIEW_CMD to a command that reads branch metadata JSON from stdin and emits scoring metrics JSON."); - if (safeStatus.reason) - guidance.push(`Last scorer error: ${safeStatus.reason}`); - break; - } - guidance.push("Local scorer output stays on your machine; LoopOver never uploads source contents."); - return guidance; -} -export function probeLocalScorer(scorerCommand = resolveScorePreviewCommand()) { - return sanitizeLocalScorerStatus(runExternalScorePreview({ - repoFullName: "JSONbored/loopover", - branchName: "doctor-probe", - changedFiles: [{ path: "src/example.ts", additions: 12, deletions: 2, status: "modified" }], - repoRoot: process.cwd(), - }, scorerCommand)); -} -function gitOutput(cwd, args) { - try { - return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }); - } - catch { - return ""; - } -} -export function gitLines(cwd, args) { - return gitOutput(cwd, args) - .split("\n") - .map((line) => line.trim()) - .filter(Boolean); -} -function collectChangedFiles(cwd, baseRef) { - // Read both halves with `-z`: the human format quotes non-ASCII/control-char paths, so a quoted - // name-status key would never match the verbatim numstat key and the file's stats would be lost. - const numstat = new Map(parseNumstat(cwd, baseRef).map((entry) => [entry.path, entry])); - return parseNameStatus(cwd, baseRef).map((entry) => { - // Defensive fallback for the two invocations disagreeing on which paths they report -- not - // reproduced by any git scenario found so far (mode/type/rename/submodule changes all agree - // between --name-status and --numstat here), kept as a genuine safety net rather than an assert. - /* v8 ignore next */ - const stats = numstat.get(entry.path) ?? { additions: 0, deletions: 0, binary: false }; - return stripUndefined({ - path: entry.path, - previousPath: entry.previousPath, - additions: stats.additions, - deletions: stats.deletions, - status: statusFromCode(entry.code), - binary: stats.binary, - }); - }); -} -function parseNameStatus(cwd, baseRef) { - // `-z`: the status code is its own field and paths are verbatim; a rename is followed by the old - // then the new path, any other status by a single path. - const records = gitOutput(cwd, ["diff", "--name-status", "-M", "-z", baseRef, "--"]).split("\0"); - const entries = []; - for (let index = 0; index < records.length; index += 1) { - const code = records[index]; - if (!code) - continue; - const isRename = code.startsWith("R"); - const previousPath = isRename ? records[index + 1] : undefined; - const path = records[index + (isRename ? 2 : 1)]; - index += isRename ? 2 : 1; - entries.push({ code, path: path, previousPath }); - } - return entries; -} -function parseNumstat(cwd, baseRef) { - // `-z`: paths are verbatim and a rename emits old/new as separate fields, not the lossy - // "{a => b}" / "a => b" human form that left cross-directory renames keyed by an unmatchable string. - const records = gitOutput(cwd, ["diff", "--numstat", "-M", "-z", baseRef, "--"]).split("\0"); - const entries = []; - for (let index = 0; index < records.length; index += 1) { - const stat = records[index]; - if (!stat) - continue; - const [added, deleted, inlinePath] = splitNumstatStat(stat); - // An empty inline path marks a rename: the new path is the second of the two following fields. - let path = inlinePath; - if (inlinePath === "") { - path = records[index + 2]; - index += 2; - } - const binary = added === "-"; - entries.push({ path: path, additions: binary ? 0 : Number(added), deletions: binary ? 0 : Number(deleted), binary }); - } - return entries; -} -function splitNumstatStat(stat) { - // "\t\t" -- keep the path slice intact even if it contains tabs. - const firstTab = stat.indexOf("\t"); - const secondTab = stat.indexOf("\t", firstTab + 1); - return [stat.slice(0, firstTab), stat.slice(firstTab + 1, secondTab), stat.slice(secondTab + 1)]; -} -function collectCommitMessages(cwd, baseRef) { - const rangeMessages = gitLines(cwd, ["log", "--pretty=%B%x1e", `${baseRef}..HEAD`]).join("\n"); - const messages = rangeMessages - .split("\u001e") - .map((message) => message.trim()) - .filter(Boolean); - if (messages.length > 0) - return messages.slice(0, 30); - const last = gitLines(cwd, ["log", "-1", "--pretty=%B"]).join("\n").trim(); - return last ? [last] : []; -} -function defaultBaseRef(cwd) { - const originHead = gitLines(cwd, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"])[0]; - if (originHead) - return originHead; - if (gitLines(cwd, ["rev-parse", "--verify", "origin/main"]).length > 0) - return "origin/main"; - if (gitLines(cwd, ["rev-parse", "--verify", "origin/master"]).length > 0) - return "origin/master"; - return "HEAD"; -} -// baseRef is always a real string here: collectRemoteTrackingSha's sole caller (collectLocalBranchMetadata) -// resolves its own `baseRef` local through `input.baseRef ?? defaultBaseRef(cwd)`, which always ends in a -// string ("HEAD"/"origin/main"/"origin/master" at worst) -- never null/undefined. -function collectRemoteTrackingSha(cwd, baseRef) { - const match = baseRef.replace(/^refs\/remotes\//, "").match(/^origin\/(.+)$/); - const branch = match?.[1]; - if (!branch) - return undefined; - const remoteRow = gitLines(cwd, ["ls-remote", "--heads", "origin", branch])[0]; - return remoteRow?.split(/\s+/)[0]; -} -function normalizeScorerOutput(payload) { - const source = payload.source; - const total = payload.total; - const tests = payload.tests; - const nonCode = payload.nonCode; - return stripUndefined({ - mode: "external_command", - activeModel: stringValue(payload.activeModel ?? payload.active_model), - sourceTokenScore: numberValue(payload.sourceTokenScore ?? payload.source_token_score ?? source?.tokenScore), - totalTokenScore: numberValue(payload.totalTokenScore ?? payload.total_token_score ?? total?.tokenScore), - sourceLines: numberValue(payload.sourceLines ?? payload.source_lines ?? source?.lines), - testTokenScore: numberValue(payload.testTokenScore ?? payload.test_token_score ?? tests?.tokenScore), - nonCodeTokenScore: numberValue(payload.nonCodeTokenScore ?? payload.non_code_token_score ?? nonCode?.tokenScore), - warnings: Array.isArray(payload.warnings) ? payload.warnings.map(String) : undefined, - }); -} -// status.reason is always set here: metadataOnlyScorer's sole caller (buildBranchAnalysisPayload) only -// invokes it with an ok:false ScorerStatus from runExternalScorePreview, and every ok:false branch there -// returns via scorerFailure(code, reason, ...), whose `reason` parameter is required and always a real -// string literal -- so the `?? status.code ?? "external_scorer_unavailable"` fallbacks can never fire. -function metadataOnlyScorer(status) { - return { - mode: "metadata_only", - warnings: [status.reason], - }; -} -function scorerFailure(code, reason, extra = {}) { - return stripUndefined({ - ok: false, - code, - reason, - fallbackMode: "metadata_only", - ...extra, - }); -} -function classifyScorerExecFailure(error, durationMs, scorerCommand) { - // execError's "not an object" fallback and message's "not an Error instance" fallback below are - // defensive: every real failure this function's sole caller (runExternalScorePreview) hands it comes - // from either a Node child_process error (always a real Error/object) or a thrown TypeError (circular - // JSON), so both fallbacks are unreachable through this codebase's own real failure modes -- kept as - // genuine defense-in-depth against a future Node/JS runtime that throws something else, not asserts. - /* v8 ignore next */ - const execError = error && typeof error === "object" ? error : undefined; - const output = execError?.output; - const stdout = String(execError?.stdout ?? output?.[1] ?? "").trim(); - const stderr = truncateText(String(execError?.stderr ?? output?.[2] ?? "")); - const exitCode = typeof execError?.status === "number" ? execError.status : undefined; - if (stdout && !looksLikeScorerJson(stdout)) { - return scorerFailure("malformed_json", "External scorer stdout was not valid JSON.", { - durationMs, - stderr: truncateText(stdout), - scorerCommand: redactScorerCommand(scorerCommand), - fallbackMode: "metadata_only", - }); - } - // execFileSync's own timeout option always kills via SIGTERM (never sets code:"ETIMEDOUT" directly in - // this Node version), so only the second half of this OR is reachable through a real timeout -- the - // first half stays as documented compatibility with Node behavior that has varied across versions. - /* v8 ignore next */ - if (execError?.code === "ETIMEDOUT" || (execError?.killed && execError?.signal === "SIGTERM")) { - return scorerFailure("timeout", `External scorer timed out after ${scorePreviewTimeoutMs()}ms.`, { durationMs, stderr, scorerCommand: redactScorerCommand(scorerCommand) }); - } - if (typeof exitCode === "number" && exitCode !== 0) { - return scorerFailure("non_zero_exit", `External scorer exited with status ${exitCode}.`, { durationMs, stderr, exitCode, scorerCommand: redactScorerCommand(scorerCommand) }); - } - /* v8 ignore next */ - const message = error instanceof Error ? error.message : "external_scorer_failed"; - if (/JSON/i.test(message)) { - return scorerFailure("malformed_json", "External scorer stdout was not valid JSON.", { durationMs, stderr, scorerCommand: redactScorerCommand(scorerCommand) }); - } - if (stderr && !looksLikeScorerJson(stderr)) { - return scorerFailure("malformed_json", "External scorer stdout was not valid JSON.", { - durationMs, - stderr: truncateText(stderr), - scorerCommand: redactScorerCommand(scorerCommand), - fallbackMode: "metadata_only", - }); - } - return scorerFailure("scorer_failed", redactLocalPath(message), { durationMs, stderr, exitCode, scorerCommand: redactScorerCommand(scorerCommand) }); -} -function looksLikeScorerJson(output) { - try { - const payload = JSON.parse(output); - // Confirmed reachable at runtime (a non-object JSON value, e.g. a bare number, hits this return - // directly -- verified by direct invocation outside the test runner and by dedicated tests below); - // the coverage tool's own attribution for this exact line is a known v8/sourcemap remapping - // artifact for compiled-from-.ts files also seen elsewhere in this migration, not a real gap. - /* v8 ignore next */ - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return false; - const normalized = normalizeScorerOutput(payload); - return normalized.sourceTokenScore !== undefined || normalized.totalTokenScore !== undefined; - } - catch { - return false; - } -} -function inferScorerCode(reason) { - const text = String(reason ?? ""); - if (text.includes("missing_scorer_command")) - return "missing_scorer_command"; - if (text.includes("empty_scorer_command")) - return "empty_scorer_command"; - if (/timed out|ETIMEDOUT/i.test(text)) - return "timeout"; - if (/JSON/i.test(text)) - return "malformed_json"; - if (/status \d+/i.test(text)) - return "non_zero_exit"; - return "scorer_failed"; -} -function scorePreviewTimeoutMs() { - const parsed = Number(process.env.GITTENSOR_SCORE_PREVIEW_TIMEOUT_MS ?? 15000); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 15000; -} -function truncateText(value, maxLength = 240) { - // Every real call site already passes a defined string (several wrap the value in String(...) first, - // and the rest hand truncateText's own prior output back in only when it's guarded by a truthy check - // just above the call) -- the `?? ""` here is a generic helper's own defensive default, unreachable - // through this file's current callers. - /* v8 ignore next */ - const text = String(value ?? "").trim(); - if (!text) - return undefined; - return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3)}...`; -} -function splitCommand(command) { - return String(command).match(/(?:[^\s"]+|"[^"]*")+/g)?.map((part) => part.replace(/^"|"$/g, "")) ?? []; -} -function assertSourceUploadDisabled() { - if (/^(1|true|yes)$/i.test(process.env.LOOPOVER_UPLOAD_SOURCE ?? "false")) { - throw new Error("LOOPOVER_UPLOAD_SOURCE=true is not supported in v1; local MCP sends metadata only."); - } -} -// Word-boundary the closing keywords (as the server-side extractors in src/db/repositories.ts and -// src/signals/engine.ts already do) so a keyword embedded in a longer word does not spuriously link an -// issue: without \b, `hotfix 5` / `prefixes 12` matched the `fix`/`fixes` substring and captured the -// trailing number. The bare `#` branch stays boundary-free so `#123` still matches anywhere. -export function extractLinkedIssues(text) { - const issues = []; - for (const match of String(text).matchAll(/(?:\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)|#)\s*#?(\d+)/gi)) - issues.push(Number(match[1])); - return issues.filter((issue) => Number.isInteger(issue) && issue > 0); -} -function statusFromCode(code) { - if (code.startsWith("A")) - return "added"; - if (code.startsWith("M")) - return "modified"; - if (code.startsWith("D")) - return "deleted"; - if (code.startsWith("R")) - return "renamed"; - // parseNameStatus's sole caller (collectChangedFiles) invokes `git diff --name-status -M` -- copy - // detection ("C" statuses) is only emitted with an explicit `-C` flag, which is never passed here, so - // this arm is unreachable through this file's own git invocation. Kept (not deleted) because it is - // still a real git status-letter git diff can produce with different flags, not a made-up case. - /* v8 ignore next -- see comment above; unreachable given collectChangedFiles' own fixed -M-only flags */ - if (code.startsWith("C")) - return "copied"; - // Reachable for real: a type change (e.g. regular file <-> symlink) reports "T" even with -M alone, - // same for unmerged ("U")/unknown ("X")/broken-pairing ("B") -- none of those map to a known status. - return "unknown"; -} -// branchName is always a real string here: titleFromBranch's sole caller (collectLocalBranchMetadata) -// resolves its own `branchName` local through a `??` chain that ends in the "local-branch" literal, so -// it is provably never null/undefined -- no `?? ""` fallback needed to keep String() safe. -function titleFromBranch(branchName) { - const title = branchName - .replace(/^[-/_.\w]+\/(?=[^/]+$)/, "") - .replace(/[-_]+/g, " ") - .trim(); - return title || undefined; -} -function firstCommitTitle(messages) { - return messages.find((message) => message.trim().length > 0)?.split("\n")[0]?.trim(); -} -function numberValue(value) { - const number = Number(value); - return Number.isFinite(number) ? number : undefined; -} -function stringValue(value) { - return typeof value === "string" && value.trim() ? value : undefined; -} -function stripUndefined(value) { - if (Array.isArray(value)) - return value.map(stripUndefined); - if (!value || typeof value !== "object") - return value; - return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).map(([key, entry]) => [key, stripUndefined(entry)])); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9jYWwtYnJhbmNoLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9jYWwtYnJhbmNoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxVQUFVLEVBQVEsUUFBUSxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUNoRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxVQUFVLEVBQUUsVUFBVSxJQUFJLFVBQVUsRUFBRSxNQUFNLHdDQUF3QyxDQUFDO0FBQzlGLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUV6RCxPQUFPLEVBQUUsVUFBVSxFQUFFLFVBQVUsRUFBRSxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxlQUFlLEVBQUUsQ0FBQztBQThFM0IsU0FBUyxvQkFBb0IsQ0FBQyxLQUFhO0lBQ3pDLElBQUksR0FBRyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUM7SUFDdkIsT0FBTyxHQUFHLEdBQUcsQ0FBQyxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxLQUFLLEVBQUU7UUFBRSxHQUFHLElBQUksQ0FBQyxDQUFDO0lBQzdELE9BQU8sR0FBRyxLQUFLLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDNUQsQ0FBQztBQUVELE1BQU0sVUFBVSxjQUFjLENBQUMsU0FBa0I7SUFDL0MsTUFBTSxPQUFPLEdBQUcsb0JBQW9CLENBQUMsTUFBTSxDQUFDLFNBQVMsSUFBSSxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3JFLE1B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 \ No newline at end of file diff --git a/packages/loopover-mcp/lib/redact-local-path.js b/packages/loopover-mcp/lib/redact-local-path.js deleted file mode 100644 index 2d1ea4d2fb..0000000000 --- a/packages/loopover-mcp/lib/redact-local-path.js +++ /dev/null @@ -1,60 +0,0 @@ -// #6264: the one shared local-filesystem-path redactor for the MCP CLI. Three call sites used to -// carry their own copy of this logic (`redactLocalPath` here, `redactLocalValidationPaths` and -// `sanitizeDiagnosticText` in bin/loopover-mcp.js), so a single redaction fix had to be made — and -// kept in sync — three times. They are consolidated here so a future fix happens once. -// -// Two genuinely different mechanisms are needed, so both stay available as named functions rather -// than being forced into one: -// - `redactLocalPath` DETECTS an unknown absolute/home path in free text via a regex heuristic -// (stack traces, scorer stderr, pasted validation output) → ``. -// - `redactKnownLocalPaths` redacts KNOWN sensitive strings (session tokens, config dirs, cwd/home) -// supplied by the caller, by exact substring substitution → `[redacted]` / -// `[local-path]`. It cannot detect an arbitrary path; the heuristic cannot -// redact a token it was never told about. Each solves a distinct problem. -/** - * Redact any absolute or home-anchored local path found in free text, replacing it with the - * `` placeholder. Heuristic (matches an unknown path by shape), so it never needs the - * concrete path in advance — the counterpart to the exact-match `redactKnownLocalPaths` below. - */ -export function redactLocalPath(value) { - const text = String(value ?? ""); - if (!text) - return text; - // Both `/g` patterns are rebuilt per call so no `lastIndex` state carries between invocations. - // Delimiter-anchored roots (`~/`, `~\`, `C:\`, `C:/`, `/`) whose interior segments may contain - // spaces, e.g. `/Users/Alice Smith/project` — the anchoring prefix is preserved, only the path swaps. - const pathSegment = "[^\\\\/\\s\"'`,;)\\]]+(?:\\s+[^\\\\/\\s\"'`,;)\\]]+)*(?=[\\\\/])"; - const pathTail = "[^\\\\/\\s\"'`,;)\\]]+"; - // Prefix delimiters a real path can immediately follow in pasted stack-trace/validation-output text. - // `(` is the Node.js stack-frame shape (`at fn (/abs/path:10:5)`); `[` and `:` cover the same "no space - // before the path" shape in bracketed log lines and colon-joined messages (e.g. `path:/abs/path`). - const rootedPath = new RegExp(`(^|[\\s"'\\\`=(\\[:])((?:~[\\\\/]|[A-Za-z]:[\\\\/]|/)(?:${pathSegment}[\\\\/])*${pathTail})`, "g"); - return text - .replace(rootedPath, (_, prefix) => `${prefix}`) - // Home/Windows roots that appear mid-token with no leading delimiter (so the anchored pass skips - // them); run second so it only mops up what the anchored, space-aware pass could not claim. - .replace(/(?:~\/|[A-Za-z]:\\)[^\s"'`,;)]+/g, ""); -} -/** - * Redact KNOWN sensitive strings from free text by exact substring substitution: every entry of - * `tokens` becomes `[redacted]` and every entry of `paths` becomes `[local-path]`. Non-string / - * empty entries are ignored; a token must be non-empty and a path longer than one character (a bare - * `/` is not a "known path"). Paths are applied longest-first so a nested path (e.g. cwd under home) - * is redacted before a shorter prefix would swallow its tail. `undefined`/`null` pass through - * untouched so callers can hand diagnostics straight in. - */ -export function redactKnownLocalPaths(value, { tokens = [], paths = [] } = {}) { - if (value === undefined || value === null) - return value; - let text = String(value); - for (const token of tokens) { - if (typeof token === "string" && token.length > 0) - text = text.split(token).join("[redacted]"); - } - const knownPaths = paths.filter((candidate) => typeof candidate === "string" && candidate.length > 1); - for (const localPath of knownPaths.sort((left, right) => right.length - left.length)) { - text = text.split(localPath).join("[local-path]"); - } - return text; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVkYWN0LWxvY2FsLXBhdGguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJyZWRhY3QtbG9jYWwtcGF0aC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxpR0FBaUc7QUFDakcsK0ZBQStGO0FBQy9GLG1HQUFtRztBQUNuRyx1RkFBdUY7QUFDdkYsRUFBRTtBQUNGLGtHQUFrRztBQUNsRyw4QkFBOEI7QUFDOUIsc0dBQXNHO0FBQ3RHLHVHQUF1RztBQUN2RyxzR0FBc0c7QUFDdEcsc0dBQXNHO0FBQ3RHLHNHQUFzRztBQUN0RyxxR0FBcUc7QUFFckc7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQUMsS0FBYztJQUM1QyxNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsS0FBSyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ2pDLElBQUksQ0FBQyxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDdkIsK0ZBQStGO0lBQy9GLCtGQUErRjtJQUMvRixzR0FBc0c7SUFDdEcsTUFBTSxXQUFXLEdBQUcsa0VBQWtFLENBQUM7SUFDdkYsTUFBTSxRQUFRLEdBQUcsd0JBQXdCLENBQUM7SUFDMUMscUdBQXFHO0lBQ3JHLHdHQUF3RztJQUN4RyxtR0FBbUc7SUFDbkcsTUFBTSxVQUFVLEdBQUcsSUFBSSxNQUFNLENBQUMsMkRBQTJELFdBQVcsWUFBWSxRQUFRLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQztJQUNsSSxPQUFPLElBQUk7U0FDUixPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQyxFQUFFLE1BQWMsRUFBRSxFQUFFLENBQUMsR0FBRyxNQUFNLGNBQWMsQ0FBQztRQUNwRSxpR0FBaUc7UUFDakcsNEZBQTRGO1NBQzNGLE9BQU8sQ0FBQyxrQ0FBa0MsRUFBRSxjQUFjLENBQUMsQ0FBQztBQUNqRSxDQUFDO0FBSUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxxQkFBcUIsQ0FDbkMsS0FBYyxFQUNkLEVBQUUsTUFBTSxHQUFHLEVBQUUsRUFBRSxLQUFLLEdBQUcsRUFBRSxLQUFtQyxFQUFFO0lBRTlELElBQUksS0FBSyxLQUFLLFNBQVMsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3hELElBQUksSUFBSSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN6QixLQUFLLE1BQU0sS0FBSyxJQUFJLE1BQU0sRUFBRSxDQUFDO1FBQzNCLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQztZQUFFLElBQUksR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztJQUNqRyxDQUFDO0lBQ0QsTUFBTSxVQUFVLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLFNBQVMsRUFBdUIsRUFBRSxDQUFDLE9BQU8sU0FBUyxLQUFLLFFBQVEsSUFBSSxTQUFTLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzNILEtBQUssTUFBTSxTQUFTLElBQUksVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDckYsSUFBSSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0lBQ3BELENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQztBQUNkLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-mcp/lib/telemetry.js b/packages/loopover-mcp/lib/telemetry.js deleted file mode 100644 index 25152e3695..0000000000 --- a/packages/loopover-mcp/lib/telemetry.js +++ /dev/null @@ -1,61 +0,0 @@ -import { PostHog } from "posthog-node"; -// Local MCP telemetry wrapper (#6236, mirrors the remote wrapper from #6235). Same allowlisted event shape -// and PostHog vendor as src/mcp/telemetry.ts, so the two servers report consistent data -- the only real -// difference is the trust posture: this CLI runs on a user's own machine, so it is gated on an EXPLICIT, -// persisted opt-in flag rather than mere env-var presence. This module stays a pure helper like its lib/ -// siblings (cli-error.js, format-table.js, ...) -- it never reads the CLI's config file itself. The caller -// (bin/loopover-mcp.js) resolves `telemetryEnabled` from the persisted config and passes it in. -// -// SAFE NO-OP: unless the caller passes `telemetryEnabled: true` AND LOOPOVER_MCP_POSTHOG_API_KEY is set, -// this records nothing and behaves byte-identically to before this module existed -- true for every user -// who has not run `loopover-mcp telemetry enable` (the default). It also never throws: a PostHog init/ -// capture failure degrades to recording nothing, so it can never affect the CLI's actual command behavior. -/** PostHog US-cloud ingestion host -- the default when LOOPOVER_MCP_POSTHOG_HOST isn't set. */ -const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"; -/** The PostHog event name every MCP tool call is recorded under (matches the remote wrapper, #6235). */ -const MCP_TOOL_CALL_EVENT = "mcp_tool_call"; -/** Anonymous, constant distinct id: this fleet telemetry carries NO per-actor identity by design (#6228), - * so every event shares one handle and there is no per-user person to build up. */ -const MCP_TELEMETRY_DISTINCT_ID = "loopover-mcp"; -/** - * Record a single local MCP tool call to PostHog. Safe no-op unless `telemetryEnabled` is explicitly - * `true` (the caller's resolved, persisted opt-in flag, default OFF -- #6236) AND - * LOOPOVER_MCP_POSTHOG_API_KEY is configured; never throws. - */ -export function recordMcpToolCall(options, event) { - // Opt-in default OFF (#6236, per #6228's privacy decision) -- unlike the remote wrapper, presence of an - // API key alone is not enough; the user must have explicitly enabled telemetry. - if (options?.telemetryEnabled !== true) - return; - const apiKey = trimmedOrUndefined(process.env.LOOPOVER_MCP_POSTHOG_API_KEY); - // Unconfigured -> record nothing, byte-identical to before this module existed. - if (!apiKey) - return; - const host = trimmedOrUndefined(process.env.LOOPOVER_MCP_POSTHOG_HOST) ?? DEFAULT_POSTHOG_HOST; - try { - const client = new PostHog(apiKey, { host, flushAt: 1, flushInterval: 0 }); - client.capture({ - distinctId: MCP_TELEMETRY_DISTINCT_ID, - event: MCP_TOOL_CALL_EVENT, - // Exactly the #6228 allowlist -- nothing more. - properties: { - tool: event.tool, - caller_type: event.callerType ?? "local", - ok: event.ok, - duration_ms: event.durationMs, - }, - // No IP-based geo enrichment: the event is anonymous fleet telemetry, not a user location. - disableGeoip: true, - }); - } - catch { - // Telemetry is best-effort and MUST NOT throw into the CLI (#6236): a PostHog init/capture failure - // degrades to recording nothing, identical to the unconfigured path above. - } -} -/** Trim a possibly-undefined env string, treating blank/whitespace as absent. */ -function trimmedOrUndefined(value) { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVsZW1ldHJ5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidGVsZW1ldHJ5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFdkMsMkdBQTJHO0FBQzNHLHlHQUF5RztBQUN6Ryx5R0FBeUc7QUFDekcseUdBQXlHO0FBQ3pHLDJHQUEyRztBQUMzRyxnR0FBZ0c7QUFDaEcsRUFBRTtBQUNGLHlHQUF5RztBQUN6Ryx5R0FBeUc7QUFDekcsdUdBQXVHO0FBQ3ZHLDJHQUEyRztBQUUzRywrRkFBK0Y7QUFDL0YsTUFBTSxvQkFBb0IsR0FBRywwQkFBMEIsQ0FBQztBQUV4RCx3R0FBd0c7QUFDeEcsTUFBTSxtQkFBbUIsR0FBRyxlQUFlLENBQUM7QUFFNUM7b0ZBQ29GO0FBQ3BGLE1BQU0seUJBQXlCLEdBQUcsY0FBYyxDQUFDO0FBS2pEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsaUJBQWlCLENBQUMsT0FBaUMsRUFBRSxLQUF1QjtJQUMxRix3R0FBd0c7SUFDeEcsZ0ZBQWdGO0lBQ2hGLElBQUksT0FBTyxFQUFFLGdCQUFnQixLQUFLLElBQUk7UUFBRSxPQUFPO0lBRS9DLE1BQU0sTUFBTSxHQUFHLGtCQUFrQixDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsNEJBQTRCLENBQUMsQ0FBQztJQUM1RSxnRkFBZ0Y7SUFDaEYsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPO0lBRXBCLE1BQU0sSUFBSSxHQUFHLGtCQUFrQixDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMseUJBQXlCLENBQUMsSUFBSSxvQkFBb0IsQ0FBQztJQUMvRixJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sR0FBRyxJQUFJLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsRUFBRSxhQUFhLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUMzRSxNQUFNLENBQUMsT0FBTyxDQUFDO1lBQ2IsVUFBVSxFQUFFLHlCQUF5QjtZQUNyQyxLQUFLLEVBQUUsbUJBQW1CO1lBQzFCLCtDQUErQztZQUMvQyxVQUFVLEVBQUU7Z0JBQ1YsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJO2dCQUNoQixXQUFXLEVBQUUsS0FBSyxDQUFDLFVBQVUsSUFBSSxPQUFPO2dCQUN4QyxFQUFFLEVBQUUsS0FBSyxDQUFDLEVBQUU7Z0JBQ1osV0FBVyxFQUFFLEtBQUssQ0FBQyxVQUFVO2FBQzlCO1lBQ0QsMkZBQTJGO1lBQzNGLFlBQVksRUFBRSxJQUFJO1NBQ25CLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxtR0FBbUc7UUFDbkcsMkVBQTJFO0lBQzdFLENBQUM7QUFDSCxDQUFDO0FBRUQsaUZBQWlGO0FBQ2pGLFNBQVMsa0JBQWtCLENBQUMsS0FBeUI7SUFDbkQsTUFBTSxPQUFPLEdBQUcsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDO0lBQzlCLE9BQU8sT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztBQUN2QyxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-mcp/tsconfig.json b/packages/loopover-mcp/tsconfig.json index 63895b7219..a67c879a19 100644 --- a/packages/loopover-mcp/tsconfig.json +++ b/packages/loopover-mcp/tsconfig.json @@ -25,8 +25,9 @@ "tsBuildInfoFile": "./.tsbuildinfo" }, // Every bin/lib runtime module is real TypeScript (#7291, phased across #7328/#7329/#7330): tsc owns - // the in-place .js emit. The glob stays the include surface; test/unit/mcp-typescript-migration-complete.test.ts - // fails closed if a hand-maintained .js orphan ever reappears. + // 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. "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. diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.d.ts b/packages/loopover-miner/bin/loopover-miner-mcp.d.ts deleted file mode 100644 index a826e9828c..0000000000 --- a/packages/loopover-miner/bin/loopover-miner-mcp.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { type EventLedger } from "../lib/event-ledger.js"; -import { type PredictionLedgerEntry } from "../lib/prediction-ledger.js"; -/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */ -export declare const MINER_PING_STATUS: { - status: string; - tool: string; -}; -export interface MinerMcpServerOptions { - /** - * Override the portfolio-queue store opener (defaults to the real on-disk store); injection seam for tests. - * Typed to the minimal read surface the dashboard tool uses, mirroring runPortfolioDashboard's own seam. - */ - initPortfolioQueue?: () => { - listQueue(repoFullName?: string | null): unknown[]; - close(): void; - }; - /** - * Override the claim-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed to - * the minimal read surface the list-claims tool uses. - */ - openClaimLedger?: () => { - listClaims(filter?: { - repoFullName?: string | null; - status?: string | null; - }): unknown[]; - close(): void; - }; - /** Override the clock used for the oldest-queued age (defaults to Date.now()); injection seam for tests. */ - nowMs?: number; - /** Override the event-ledger opener (defaults to initEventLedger); injection seam for tests. */ - initEventLedger?: () => EventLedger; - /** - * Override the run-state store opener (defaults to the real on-disk store); injection seam for tests. Typed to - * the minimal read surface the run-state tool uses (never setRunState). - */ - initRunStateStore?: () => { - getRunState(repoFullName: string): unknown; - listRunStates(): unknown[]; - close(): void; - }; - /** - * Override the plan-store opener (defaults to the real on-disk store); injection seam for tests. Typed to the - * minimal read surface the plan tools use (never savePlan). - */ - openPlanStore?: () => { - loadPlan(planId: string): unknown; - listPlans(filter?: { - status?: string | null; - }): unknown[]; - close(): void; - }; - /** - * Override the governor-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed - * to the minimal read surface the decisions tool uses (the payload-excluding readGovernorDecisions). - */ - initGovernorLedger?: () => { - readGovernorDecisions(filter?: { - repoFullName?: string | null; - }): unknown[]; - close(): void; - }; - /** Override the status reader (defaults to status.js's collectStatus); injection seam for tests. */ - collectStatus?: () => unknown; - /** Override the doctor-checks reader (defaults to status.js's runDoctorChecks); injection seam for tests. */ - runDoctorChecks?: () => unknown[]; - /** - * Override the prediction-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed - * to the minimal read surface the calibration-report tool uses (never appendPrediction). - */ - initPredictionLedger?: () => { - readPredictions(filter?: { - repoFullName?: string | null; - }): PredictionLedgerEntry[]; - close(): void; - }; -} -/** - * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, - * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`, - * `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default - * to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its - * store only when invoked and closes any store it opened. - */ -export declare function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer; diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.js b/packages/loopover-miner/bin/loopover-miner-mcp.js deleted file mode 100755 index 77fa62342a..0000000000 --- a/packages/loopover-miner/bin/loopover-miner-mcp.js +++ /dev/null @@ -1,355 +0,0 @@ -#!/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"; -import { z } from "zod"; -import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js"; -import { collectEventLedgerAuditFeed, normalizeAuditFeedMcpFilter, } from "../lib/event-ledger-cli.js"; -import { initEventLedger } from "../lib/event-ledger.js"; -import { collectManageStatus, collectRunPortfolio } from "../lib/manage-status.js"; -import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js"; -import { initPortfolioQueueStore } from "../lib/portfolio-queue.js"; -import { initRunStateStore } from "../lib/run-state.js"; -import { PLAN_STATUSES, openPlanStore } from "../lib/plan-store.js"; -import { initGovernorLedger } from "../lib/governor-ledger.js"; -import { collectStatus, runDoctorChecks } from "../lib/status.js"; -import { buildCalibrationReport } from "../lib/calibration.js"; -import { toOutcomeRecords, toPredictionRecords } from "../lib/calibration-cli.js"; -import { initPredictionLedger } from "../lib/prediction-ledger.js"; -import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; -import { installCliSignalHandlers } from "../lib/process-lifecycle.js"; -import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; -// MCP stdio server for @loopover/miner (scaffold #5153). Mirrors the packages/loopover-mcp -// harness (MCP SDK server + stdio transport). Tools: -// - loopover_miner_ping (#5153): trivial static health check, reads no AMS state. -// - loopover_miner_get_portfolio_dashboard (#5155): read-only per-repo backlog dashboard, wrapping the -// existing collectPortfolioDashboard aggregator (no new logic; same data as `queue dashboard --json`). -// - loopover_miner_get_manage_status (#5822): read-only manage-phase status joining the portfolio queue, the -// event ledger, and run-state via manage-status.js's collectManageStatus/collectRunPortfolio (no new join -// logic; same { rows, runPortfolio } shape as `manage status --json`). Never calls GitHub, never mutates. -// - loopover_miner_list_claims (#5156): read-only listing of the local claim ledger (optional repo/status -// filter passed through to listClaims); exposes no claim/release mutation. -// - loopover_miner_get_audit_feed (#5158): read-only metadata-only event-ledger audit feed via -// collectEventLedgerAuditFeed() (same filters as `ledger list`; never returns payload_json). -// - loopover_miner_get_run_state (#5160): read-only per-repo run-state via run-state.js's getRunState/ -// listRunStates (read-only analog of ORB's loopover_get_automation_state; no state-set mutation). -// - loopover_miner_list_plans / loopover_miner_get_plan (#5161): read-only access to the persisted -// plan store via plan-store.js's listPlans/loadPlan (distinct from ORB's stateless loopover_plan_status). -// - loopover_miner_get_governor_decisions (#5159): read-only governor decision-log projection via -// governor-ledger.js's readGovernorDecisions -- an explicit named-column read that excludes payload_json. -// - loopover_miner_status (#5154): read-only status + doctor diagnostics via status.js's collectStatus/ -// runDoctorChecks (names/booleans/paths only -- never any env-var value, token, key, or credential). -// - loopover_miner_get_calibration_report (#5821): read-only miner-local prediction-accuracy report, joining -// the prediction ledger with observed pr_outcome events via calibration-cli.js's existing toPredictionRecords/ -// toOutcomeRecords mappers and calibration.js's buildCalibrationReport composer (no new join logic). Distinct -// 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"); -const ownPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); -/** Optional filters accepted by loopover_miner_get_audit_feed (#5158). */ -const auditFeedInputSchema = { - repoFullName: z.string().min(1).optional(), - since: z.number().int().nonnegative().optional(), - type: z.string().min(1).optional(), -}; -/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */ -export const MINER_PING_STATUS = { status: "ok", tool: "loopover_miner_ping" }; -/** - * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, - * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`, - * `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default - * to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its - * store only when invoked and closes any store it opened. - */ -export function createMinerMcpServer(options = {}) { - const server = new McpServer({ name: "loopover-miner", version: ownPackageJson.version }); - server.registerTool("loopover_miner_ping", { - description: "Health check for the loopover-miner MCP server. Returns a static status object confirming the " + - "server is reachable. Reads no AMS state and takes no arguments.", - inputSchema: {}, - }, async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] })); - server.registerTool("loopover_miner_get_portfolio_dashboard", { - description: "Read-only per-repo portfolio-queue backlog dashboard: status counts (queued/in_progress/done), totals, " + - "and the oldest-queued age in ms. Wraps the existing collectPortfolioDashboard aggregator (no new logic) " + - "-- the same data `loopover-miner queue dashboard --json` prints locally. Takes no arguments; mutates nothing.", - inputSchema: {}, - }, async () => { - const ownsQueue = options.initPortfolioQueue === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - try { - const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() }); - return { content: [{ type: "text", text: JSON.stringify(summary) }] }; - } - finally { - if (ownsQueue) - portfolioQueue.close(); - } - }); - server.registerTool("loopover_miner_get_manage_status", { - description: "Read-only manage-phase status: the per-managed-PR rows `loopover-miner manage status` reports (branch, CI " + - "state, gate verdict, outcome, last-polled-at, queue status/priority) plus the run-level portfolio view " + - "(one row per tracked repo: run state, updated-at, PR count). Joins the portfolio queue, the append-only " + - "event ledger, and run-state by reusing the existing collectManageStatus/collectRunPortfolio aggregators " + - "-- no new join logic -- returning the same { rows, runPortfolio } shape `manage status --json` prints. " + - "Read-only: never calls GitHub, never mutates local stores. Takes no arguments.", - inputSchema: {}, - }, async () => { - const ownsPortfolioQueue = options.initPortfolioQueue === undefined; - const ownsEventLedger = options.initEventLedger === undefined; - const ownsRunStateStore = options.initRunStateStore === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - const runStateStore = (options.initRunStateStore ?? initRunStateStore)(); - try { - // The injection seams above are typed to the minimal read surface each tool touches (mirroring the - // dashboard tool's `{ listQueue }` seam), but collectManageStatus/collectRunPortfolio's declared source - // types name the full stores. Both aggregators only ever read (listQueue/getRunState/listRunStates) at - // runtime -- exactly what the seam guarantees -- so widening the resolved stores back to the store types - // the signatures ask for is sound; it never reaches a write/lifecycle method the minimal seam omits. - const rows = collectManageStatus({ - portfolioQueue: portfolioQueue, - eventLedger, - }); - const runPortfolio = collectRunPortfolio({ - portfolioQueue: portfolioQueue, - eventLedger, - runStateStore: runStateStore, - }); - return { content: [{ type: "text", text: JSON.stringify({ rows, runPortfolio }) }] }; - } - finally { - if (ownsPortfolioQueue) - portfolioQueue.close(); - if (ownsEventLedger) - eventLedger.close(); - if (ownsRunStateStore) - runStateStore.close(); - } - }); - server.registerTool("loopover_miner_list_claims", { - description: "Read-only listing of the local claim ledger: which issues this miner has claimed (repo, issue number, " + - "status, claimed-at, note). Optional repoFullName/status filters pass through to the existing listClaims " + - "query. Exposes no claim/release mutation and no conflict-resolution logic.", - inputSchema: { - repoFullName: z.string().optional(), - status: z.enum(CLAIM_STATUSES).optional(), - }, - }, async ({ repoFullName, status }) => { - const ownsLedger = options.openClaimLedger === undefined; - const ledger = (options.openClaimLedger ?? openClaimLedger)(); - try { - const filter = {}; - if (repoFullName !== undefined) - filter.repoFullName = repoFullName; - if (status !== undefined) - filter.status = status; - return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] }; - } - finally { - if (ownsLedger) - ledger.close(); - } - }); - server.registerTool("loopover_miner_get_audit_feed", { - description: "Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " + - "outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " + - "the same read filters as `loopover-miner ledger list` (--repo, --since, --type). Never returns " + - "payload_json or other raw ledger columns; never writes to the ledger.", - inputSchema: auditFeedInputSchema, - }, async (input) => { - const ownsLedger = options.initEventLedger === undefined; - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - try { - // zod's `.optional()` widens each field to `string | undefined`, whereas normalizeAuditFeedMcpFilter's - // input type spells the same absent-field slot as `string | null`; the normalizer treats missing and - // null identically, so narrowing the parsed input to that shape is exact, not a behavior change. - const filter = normalizeAuditFeedMcpFilter((input ?? {})); - const feed = collectEventLedgerAuditFeed(eventLedger, filter); - return { content: [{ type: "text", text: JSON.stringify(feed) }] }; - } - catch (error) { - return { - content: [ - { - type: "text", - text: JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - }), - }, - ], - isError: true, - }; - } - finally { - if (ownsLedger) - eventLedger.close(); - } - }); - server.registerTool("loopover_miner_get_run_state", { - description: "Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " + - "repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " + - "read-only analog of ORB's loopover_get_automation_state; adds no state-set or mutation capability.", - inputSchema: { - repoFullName: z.string().min(1).optional(), - }, - }, async ({ repoFullName }) => { - const ownsStore = options.initRunStateStore === undefined; - const store = (options.initRunStateStore ?? initRunStateStore)(); - try { - const result = repoFullName === undefined - ? { states: store.listRunStates() } - : { repoFullName, state: store.getRunState(repoFullName) }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; - } - finally { - if (ownsStore) - store.close(); - } - }); - server.registerTool("loopover_miner_list_plans", { - description: "Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " + - "filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " + - "this is the store-backed AMS plan store; it is distinct from ORB's stateless loopover_plan_status " + - "tool, which reads the caller's in-memory plan object rather than any persisted store.", - inputSchema: { - status: z.enum(PLAN_STATUSES).optional(), - }, - }, async ({ status }) => { - const ownsStore = options.openPlanStore === undefined; - const store = (options.openPlanStore ?? openPlanStore)(); - try { - const filter = {}; - if (status !== undefined) - filter.status = status; - return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] }; - } - finally { - if (ownsStore) - store.close(); - } - }); - server.registerTool("loopover_miner_get_plan", { - description: "Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " + - "explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " + - "no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " + - "loopover_plan_status tool.", - inputSchema: { - planId: z.string().min(1), - }, - }, async ({ planId }) => { - const ownsStore = options.openPlanStore === undefined; - const store = (options.openPlanStore ?? openPlanStore)(); - try { - const plan = store.loadPlan(planId); - const result = plan === null ? { planId, found: false } : { found: true, plan }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; - } - finally { - if (ownsStore) - store.close(); - } - }); - server.registerTool("loopover_miner_get_governor_decisions", { - description: "Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " + - "decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " + - "(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " + - "explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " + - "supports natively). Read-only; never writes to the ledger.", - inputSchema: { - repoFullName: z.string().min(1).optional(), - }, - }, async ({ repoFullName }) => { - const ownsLedger = options.initGovernorLedger === undefined; - const ledger = (options.initGovernorLedger ?? initGovernorLedger)(); - try { - const filter = {}; - if (repoFullName !== undefined) - filter.repoFullName = repoFullName; - return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] }; - } - finally { - if (ownsLedger) - ledger.close(); - } - }); - server.registerTool("loopover_miner_status", { - description: "Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " + - "(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " + - "name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " + - "`loopover-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " + - "collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " + - "any env-var value, token, key, or credential. Read-only; no writes or state changes.", - inputSchema: {}, - }, async () => { - const status = (options.collectStatus ?? collectStatus)(); - const doctor = (options.runDoctorChecks ?? runDoctorChecks)(); - return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] }; - }); - server.registerTool("loopover_miner_get_calibration_report", { - description: "Read-only miner-local prediction-accuracy report: per-project merge/close precision, joining this " + - "miner's own recorded gate predictions (prediction ledger) with the realized PR outcomes it later " + - "observed (pr_outcome events). Wraps calibration-cli.js's existing toPredictionRecords/toOutcomeRecords " + - "mappers and calibration.js's buildCalibrationReport composer -- no new join/scoring logic, no mutation. " + - "Strictly local and offline; distinct from ORB's hosted, maintainer-authenticated " + - "loopover_get_outcome_calibration tool, which reads a different (D1) data source. Takes no arguments.", - inputSchema: {}, - }, async () => { - const ownsPredictionLedger = options.initPredictionLedger === undefined; - const ownsEventLedger = options.initEventLedger === undefined; - let predictionLedger; - let eventLedger; - try { - predictionLedger = (options.initPredictionLedger ?? initPredictionLedger)(); - eventLedger = (options.initEventLedger ?? initEventLedger)(); - const report = buildCalibrationReport(toPredictionRecords(predictionLedger.readPredictions()), toOutcomeRecords(eventLedger.readEvents())); - return { content: [{ type: "text", text: JSON.stringify(report) }] }; - } - finally { - if (ownsPredictionLedger) - predictionLedger?.close(); - if (ownsEventLedger) - eventLedger?.close(); - } - }); - return server; -} -// Start the stdio transport only when executed directly as the bin, not when imported by a test. -// realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches. -const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : ""; -/* v8 ignore start -- process entry point: this guard is specifically what makes it unreachable when the - * module is imported (every existing MCP tool test does exactly that, per this file's own comment above), so - * it can never be true in this test run's own process. createMinerMcpServer() itself is fully exercised by - * those tests; this is only the top-level "am I actually invoked as the bin" wiring, mirroring - * loopover-miner.js's identical exemption and src/server.ts's in codecov.yml. */ -if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) { - // Previously this bin had NO crash safety net beyond the startup .catch() below -- an exception thrown while - // handling an MCP tool call, after the server was already connected, had nowhere to go (#6011). Wire in the - // same opt-in Sentry + signal/crash handling loopover-miner.js already gets, sharing process-lifecycle.js. - try { - loadMinerFileSecrets(); - } - catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - } - await initMinerSentry(process.env); - installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); - createMinerMcpServer() - .connect(new StdioServerTransport()) - .catch(async (error) => { - console.error(error); - // Awaited so the captured event has a chance to actually reach Sentry before exit() tears the process - // down -- a bare synchronous capture only queues it (#6011 follow-up). - await captureMinerErrorAndFlush(error, { kind: "mcp_startup_connect_failed" }); - process.exit(1); - }); -} -/* v8 ignore stop */ -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcG92ZXItbWluZXItbWNwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9vcG92ZXItbWluZXItbWNwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFDQSxPQUFPLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyRCxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUNwRSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUNqRixPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBQ3hCLE9BQU8sRUFBRSxjQUFjLEVBQUUsZUFBZSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDekUsT0FBTyxFQUVMLDJCQUEyQixFQUMzQiwyQkFBMkIsR0FDNUIsTUFBTSw0QkFBNEIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsZUFBZSxFQUFvQixNQUFNLHdCQUF3QixDQUFDO0FBQzNFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ25GLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzFFLE9BQU8sRUFBRSx1QkFBdUIsRUFBNEIsTUFBTSwyQkFBMkIsQ0FBQztBQUM5RixPQUFPLEVBQUUsaUJBQWlCLEVBQXNCLE1BQU0scUJBQXFCLENBQUM7QUFDNUUsT0FBTyxFQUFFLGFBQWEsRUFBRSxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNwRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUMvRCxPQUFPLEVBQUUsYUFBYSxFQUFFLGVBQWUsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2xFLE9B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 \ No newline at end of file diff --git a/packages/loopover-miner/bin/loopover-miner.d.ts b/packages/loopover-miner/bin/loopover-miner.d.ts deleted file mode 100644 index b7988016da..0000000000 --- a/packages/loopover-miner/bin/loopover-miner.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -export {}; diff --git a/packages/loopover-miner/bin/loopover-miner.js b/packages/loopover-miner/bin/loopover-miner.js deleted file mode 100755 index bf2fa78c52..0000000000 --- a/packages/loopover-miner/bin/loopover-miner.js +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env node -import { runAttempt } from "../lib/attempt-cli.js"; -import { printHelp, printVersion, runCli } from "../lib/cli.js"; -import { configureLogger, extractLogOptions } from "../lib/logger.js"; -import { runDenyCheck } from "../lib/deny-check.js"; -import { runDiscover } from "../lib/discover-cli.js"; -import { runFeasibilityCli } from "../lib/feasibility-cli.js"; -import { runIdeaFeasibilityCli } from "../lib/idea-feasibility-cli.js"; -import { runGovernorCli } from "../lib/governor-ledger-cli.js"; -import { runLedgerCli } from "../lib/event-ledger-cli.js"; -import { runCalibrationCli } from "../lib/calibration-cli.js"; -import { runLoop } from "../lib/loop-cli.js"; -import { runManagePoll } from "../lib/manage-poll.js"; -import { runManageStatus } from "../lib/manage-status.js"; -import { runMetrics } from "../lib/metrics-cli.js"; -import { runPlanCli } from "../lib/plan-store-cli.js"; -import { runClaimCli } from "../lib/claim-ledger-cli.js"; -import { runPurge } from "../lib/purge-cli.js"; -import { runQueueCli } from "../lib/portfolio-queue-cli.js"; -import { runOrbExportCli } from "../lib/orb-export.js"; -import { runTenantCli } from "../lib/tenant-cli.js"; -import { installCliSignalHandlers } from "../lib/process-lifecycle.js"; -import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; -import { runStateCli } from "../lib/run-state-cli.js"; -import { runInit } from "../lib/laptop-init.js"; -import { createWizardIo, runInteractiveInit } from "../lib/init-wizard.js"; -import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; -import { runMigrate } from "../lib/migrate-cli.js"; -import { runDoctor, runStatus } from "../lib/status.js"; -import { awaitOpportunisticUpdateCheck, resolveUpgradeCommand, startUpdateCheck, } from "../lib/update-check.js"; -import { resolveMinerVersion } from "../lib/version.js"; -// Resolve any `_FILE` secret-mount vars (GITHUB_TOKEN_FILE, etc.) into their plain counterparts FIRST, -// before anything else reads process.env -- every subcommand below (and the coding-agent driver construction -// deeper in the call graph) reads plain env vars, so this single early pass is all that's needed for the whole -// CLI (#5178). A broken secret mount fails the process fast and loud with a clear message, instead of an -// uncaught-exception stack trace or a silent empty credential surfacing as a confusing GitHub 401 later. -// Exits 2, not 1: docs/unattended-scheduling.md's contract only defines 0 (success) and 2 (failure -- "Alert -// on this"), so an operator alerting strictly on 2 would otherwise miss a broken secret mount entirely. -try { - loadMinerFileSecrets(); -} -catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(2); -} -// Opt-in Sentry (#6011): a complete no-op unless the operator sets LOOPOVER_MINER_SENTRY_DSN themselves. Must -// run AFTER loadMinerFileSecrets (so a `_FILE`-mounted DSN resolves first) and BEFORE installCliSignalHandlers -// (so a startup crash is still captured). -/* v8 ignore start -- process entry point (this bin's top level runs unconditionally, every invocation); - * exercised by the real --help/--version subprocess spawn in miner-package-skeleton.test.ts (a different - * Node process, invisible to this test run's own coverage instrumentation), not unit-coverable here. The - * functions themselves (initMinerSentry, installCliSignalHandlers) are fully unit-tested in isolation - * (miner-sentry.test.ts, miner-process-lifecycle.test.ts) -- this is only the top-level wiring that calls - * them, mirroring src/server.ts's identical, already-established exemption in codecov.yml. */ -await initMinerSentry(process.env); -// Register signal + crash handlers once, before any command runs, so an interrupted run closes its open ledgers -// cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones. -installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); -/* v8 ignore stop */ -// Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the -// process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing -// them; the stripped `cliArgs` is what the command dispatch sees. -const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2)); -configureLogger({ ...logOptions, env: process.env }); -// `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls. -// `init` stays local by default and only makes a network call when the operator explicitly passes -// `--verify-token`. -// Dispatch the local commands BEFORE the opportunistic npm-registry update check is even started, so they can -// never reach that network path (the update check runs for the remaining commands below). -if (cliArgs[0] === "init") { - if (cliArgs.includes("--interactive")) { - const wizardIo = createWizardIo(); - try { - process.exit(await runInteractiveInit(process.env, process.cwd(), wizardIo)); - } - finally { - wizardIo.close(); - } - } - process.exit(await runInit(cliArgs.slice(1))); -} -if (cliArgs[0] === "status") { - process.exit(runStatus(cliArgs.slice(1))); -} -if (cliArgs[0] === "doctor") { - process.exit(runDoctor(cliArgs.slice(1))); -} -// `migrate` is strictly local + offline like `status`/`doctor` (it only opens the local SQLite stores), so it is -// dispatched here too, before the opportunistic npm-registry update check is ever started. -if (cliArgs[0] === "migrate") { - process.exit(runMigrate(cliArgs.slice(1))); -} -// `metrics` is strictly local + offline like `status`/`doctor` (it reads only the local prediction ledger), so it -// is dispatched here, before the opportunistic npm-registry update check is ever started. -if (cliArgs[0] === "metrics") { - process.exit(runMetrics(cliArgs.slice(1))); -} -if (cliArgs[0] === "manage" && cliArgs[1] === "status") { - process.exit(runManageStatus(cliArgs.slice(2))); -} -if (cliArgs[0] === "queue") { - process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "orb" && cliArgs[1] === "export") { - process.exit(await runOrbExportCli(cliArgs.slice(2))); -} -// `tenant` (#7275) talks to the hosting control-plane's provisioning API — a deliberate, Bearer-authed admin -// action that is inert unless LOOPOVER_MINER_CONTROL_PLANE is set. Grouped with `orb export` above (also a -// network command) rather than the strictly-local commands; it fails loud on any control-plane error. -if (cliArgs[0] === "tenant") { - process.exit(await runTenantCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "claim") { - process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "ledger") { - process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "calibration") { - process.exit(runCalibrationCli(cliArgs.slice(1))); -} -if (cliArgs[0] === "plan") { - process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "governor") { - process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2))); -} -if (cliArgs[0] === "feasibility") { - process.exit(runFeasibilityCli(cliArgs.slice(1))); -} -if (cliArgs[0] === "idea-feasibility") { - process.exit(runIdeaFeasibilityCli(cliArgs.slice(1))); -} -// `purge` (#5564) is strictly local + offline like `queue`/`claim`/`governor` above -- it only opens the local -// SQLite stores, so it is dispatched here too, before the opportunistic npm-registry update check ever starts. -if (cliArgs[0] === "purge") { - process.exit(runPurge(cliArgs.slice(1))); -} -const packageName = "@loopover/miner"; -const packageVersion = resolveMinerVersion(process.env); -const upgradeCommand = resolveUpgradeCommand(packageName); -const updateCheck = startUpdateCheck(cliArgs, { - packageName, - packageVersion, - upgradeCommand, - env: process.env, -}); -if (cliArgs.length === 0 || - cliArgs.includes("--help") || - cliArgs.includes("-h") || - cliArgs[0] === "help") { - printHelp({ packageName }); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(0); -} -if (cliArgs.includes("--version") || - cliArgs.includes("-v") || - cliArgs[0] === "version") { - printVersion({ packageName, packageVersion }); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(0); -} -if (cliArgs[0] === "hooks" && cliArgs[1] === "check") { - const exitCode = runDenyCheck(cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -if (cliArgs[0] === "state") { - const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -if (cliArgs[0] === "manage" && cliArgs[1] === "poll") { - const exitCode = await runManagePoll(cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -if (cliArgs[0] === "discover") { - const exitCode = await runDiscover(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -if (cliArgs[0] === "attempt") { - const exitCode = await runAttempt(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -if (cliArgs[0] === "loop") { - const exitCode = await runLoop(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); -} -const exitCode = runCli(cliArgs, { packageName }); -await awaitOpportunisticUpdateCheck(updateCheck); -process.exit(exitCode); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcG92ZXItbWluZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJsb29wb3Zlci1taW5lci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQ0EsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUNoRSxPQUFPLEVBQUUsZUFBZSxFQUFFLGlCQUFpQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3BELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNyRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUM5RCxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUN2RSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDL0QsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQzFELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzlELE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUM3QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzFELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDdEQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3pELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEVBQUUsV0FBVyxF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 \ No newline at end of file diff --git a/packages/loopover-miner/docs/env-reference.md b/packages/loopover-miner/docs/env-reference.md index b95e770147..c9f3a42c3f 100644 --- a/packages/loopover-miner/docs/env-reference.md +++ b/packages/loopover-miner/docs/env-reference.md @@ -4,43 +4,43 @@ Generated by `npm run miner:env-reference`. Do not edit manually. | Name | First reference | Default | | --- | --- | --- | -| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.js` | `""` | -| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.js` | `""` | -| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.js` | `""` | -| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.js` | (none) | -| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.js` | (none) | -| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.js` | (none) | -| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.js` | `""` | -| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.js` | (none) | -| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.js` | `""` | -| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.js` | `""` | -| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.js` | `""` | -| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.js` | `""` | -| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.js` | `""` | -| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.js` | `""` | -| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `lib/discovery-index-client.js` | `""` | -| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `lib/discovery-index-client.js` | `""` | -| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.js` | (none) | -| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | (none) | -| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) | -| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.js` | `""` | -| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` | -| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` | -| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | (none) | -| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.js` | (none) | -| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.js` | (none) | -| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.js` | (none) | -| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.js` | (none) | -| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.js` | (none) | -| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.js` | (none) | -| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.js` | (none) | -| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.js` | (none) | -| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `"production"` | -| `LOOPOVER_MINER_VERSION` | `lib/version.js` | `""` | -| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.js` | (none) | -| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.js` | `""` | +| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.ts` | `""` | +| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.ts` | `""` | +| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.ts` | `""` | +| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.ts` | (none) | +| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.ts` | (none) | +| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.ts` | (none) | +| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.ts` | `""` | +| `LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB` | `lib/contribution-profile-cache.ts` | (none) | +| `LOOPOVER_MINER_CONTROL_PLANE` | `lib/tenant-client.ts` | `""` | +| `LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN` | `lib/tenant-client.ts` | `""` | +| `LOOPOVER_MINER_CONTROL_PLANE_URL` | `lib/tenant-client.ts` | `""` | +| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.ts` | `""` | +| `LOOPOVER_MINER_DISCOVERY_INDEX_URL` | `lib/discovery-index-client.ts` | `""` | +| `LOOPOVER_MINER_DISCOVERY_PLANE` | `lib/discovery-index-client.ts` | `""` | +| `LOOPOVER_MINER_DISCOVERY_SHARED_SECRET` | `lib/discovery-index-client.ts` | `""` | +| `LOOPOVER_MINER_DISCOVERY_TELEMETRY` | `lib/discovery-index-client.ts` | `""` | +| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.ts` | (none) | +| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.ts` | (none) | +| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.ts` | (none) | +| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.ts` | `""` | +| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.ts` | `""` | +| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.ts` | `""` | +| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.ts` | (none) | +| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.ts` | (none) | +| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.ts` | (none) | +| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.ts` | (none) | +| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.ts` | (none) | +| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.ts` | (none) | +| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.ts` | (none) | +| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.ts` | (none) | +| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.ts` | (none) | +| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.ts` | `"production"` | +| `LOOPOVER_MINER_VERSION` | `lib/version.ts` | `""` | +| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.ts` | (none) | +| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.ts` | `""` | | `MINER_CODING_AGENT_CLAUDE_MODEL` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) | | `MINER_CODING_AGENT_CODEX_MODEL` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) | | `MINER_CODING_AGENT_PAUSED` | `packages/loopover-engine/src/miner/coding-agent-mode.ts` | `""` | -| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.js` | `""` | +| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.ts` | `""` | | `MINER_CODING_AGENT_TIMEOUT_MS` | `packages/loopover-engine/src/miner/driver-factory.ts` | (none) | diff --git a/packages/loopover-miner/lib/ams-health-server.d.ts b/packages/loopover-miner/lib/ams-health-server.d.ts deleted file mode 100644 index 6173e344cf..0000000000 --- a/packages/loopover-miner/lib/ams-health-server.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type Server } from "node:http"; -export type ReadinessProbe = { - name: string; - check: () => Promise; -}; -export type Readiness = { - ok: boolean; - checks: Record; - durationsMs: Record; -}; -/** Bare liveness body: the process is up and answering, independent of any backend it depends on. */ -export declare function buildHealthBody(): { - status: "ok"; -}; -/** - * Readiness: run every injected probe and report per-probe pass/fail plus how long each took. `ok` is true only - * when every probe passed -- a container that can't reach a backend it depends on must stop reporting ready so - * the fleet aggregator can route around it. A probe that throws counts as failed (never crashes readiness), and - * its duration is still recorded. Mirrors src/selfhost/health.ts's `readiness`/`timedReadinessCheck` behavior. - */ -export declare function readiness(probes?: ReadinessProbe[]): Promise; -/** - * Build the request handler for the AMS health surface: `GET /health` -> 200 liveness, `GET /ready` -> 200/503 - * readiness (503 when any probe fails, so a load balancer stops routing to a degraded container), anything else - * -> 404. Exported separately from {@link startAmsHealthServer} so it can be exercised without binding a socket. - */ -export declare function createAmsHealthHandler(probes?: ReadinessProbe[]): (req: { - method?: string | undefined; - url?: string | undefined; -}, res: { - writeHead: (status: number, headers: Record) => void; - end: (body: string) => void; -}) => Promise; -/** - * Start the AMS health HTTP server. Resolves once it is listening. `port: 0` binds an ephemeral port (the caller - * reads `server.address()`), which is what the tests use. The hosted-container entry point owns the lifecycle and - * passes the AMS-specific probes (store reachable, loop cycle alive); the returned server is closed on shutdown. - */ -export declare function startAmsHealthServer(options?: { - port?: number; - host?: string; - probes?: ReadinessProbe[]; -}): Promise; diff --git a/packages/loopover-miner/lib/ams-health-server.js b/packages/loopover-miner/lib/ams-health-server.js deleted file mode 100644 index fe35310f40..0000000000 --- a/packages/loopover-miner/lib/ams-health-server.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createServer } from "node:http"; -/** Bare liveness body: the process is up and answering, independent of any backend it depends on. */ -export function buildHealthBody() { - return { status: "ok" }; -} -/** - * Readiness: run every injected probe and report per-probe pass/fail plus how long each took. `ok` is true only - * when every probe passed -- a container that can't reach a backend it depends on must stop reporting ready so - * the fleet aggregator can route around it. A probe that throws counts as failed (never crashes readiness), and - * its duration is still recorded. Mirrors src/selfhost/health.ts's `readiness`/`timedReadinessCheck` behavior. - */ -export async function readiness(probes = []) { - const checks = {}; - const durationsMs = {}; - let ok = true; - for (const probe of probes) { - const startedAt = performance.now(); - let passed = false; - try { - passed = (await probe.check()) === true; - } - catch { - passed = false; - } - finally { - durationsMs[probe.name] = Math.max(0, performance.now() - startedAt); - } - checks[probe.name] = passed; - if (!passed) - ok = false; - } - return { ok, checks, durationsMs }; -} -function sendJson(res, status, body) { - const payload = JSON.stringify(body); - res.writeHead(status, { "content-type": "application/json" }); - res.end(payload); -} -/** - * Build the request handler for the AMS health surface: `GET /health` -> 200 liveness, `GET /ready` -> 200/503 - * readiness (503 when any probe fails, so a load balancer stops routing to a degraded container), anything else - * -> 404. Exported separately from {@link startAmsHealthServer} so it can be exercised without binding a socket. - */ -export function createAmsHealthHandler(probes = []) { - return async (req, res) => { - const path = (req.url ?? "").split("?", 1)[0]; - if (req.method === "GET" && path === "/health") { - sendJson(res, 200, buildHealthBody()); - return; - } - if (req.method === "GET" && path === "/ready") { - const result = await readiness(probes); - sendJson(res, result.ok ? 200 : 503, result); - return; - } - sendJson(res, 404, { error: "not_found" }); - }; -} -/** - * Start the AMS health HTTP server. Resolves once it is listening. `port: 0` binds an ephemeral port (the caller - * reads `server.address()`), which is what the tests use. The hosted-container entry point owns the lifecycle and - * passes the AMS-specific probes (store reachable, loop cycle alive); the returned server is closed on shutdown. - */ -export function startAmsHealthServer(options = {}) { - const port = Number.isInteger(options.port) ? options.port : 0; - const host = typeof options.host === "string" && options.host ? options.host : "0.0.0.0"; - const probes = Array.isArray(options.probes) ? options.probes : []; - const server = createServer(createAmsHealthHandler(probes)); - return new Promise((resolve) => { - server.listen(port, host, () => resolve(server)); - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYW1zLWhlYWx0aC1zZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhbXMtaGVhbHRoLXNlcnZlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsWUFBWSxFQUFlLE1BQU0sV0FBVyxDQUFDO0FBa0J0RCxxR0FBcUc7QUFDckcsTUFBTSxVQUFVLGVBQWU7SUFDN0IsT0FBTyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUMxQixDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFNBQVMsQ0FBQyxTQUEyQixFQUFFO0lBQzNELE1BQU0sTUFBTSxHQUE0QixFQUFFLENBQUM7SUFDM0MsTUFBTSxXQUFXLEdBQTJCLEVBQUUsQ0FBQztJQUMvQyxJQUFJLEVBQUUsR0FBRyxJQUFJLENBQUM7SUFDZCxLQUFLLE1BQU0sS0FBSyxJQUFJLE1BQU0sRUFBRSxDQUFDO1FBQzNCLE1BQU0sU0FBUyxHQUFHLFdBQVcsQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUNwQyxJQUFJLE1BQU0sR0FBRyxLQUFLLENBQUM7UUFDbkIsSUFBSSxDQUFDO1lBQ0gsTUFBTSxHQUFHLENBQUMsTUFBTSxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUMsS0FBSyxJQUFJLENBQUM7UUFDMUMsQ0FBQztRQUFDLE1BQU0sQ0FBQztZQUNQLE1BQU0sR0FBRyxLQUFLLENBQUM7UUFDakIsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsV0FBVyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxXQUFXLENBQUMsR0FBRyxFQUFFLEdBQUcsU0FBUyxDQUFDLENBQUM7UUFDdkUsQ0FBQztRQUNELE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsTUFBTSxDQUFDO1FBQzVCLElBQUksQ0FBQyxNQUFNO1lBQUUsRUFBRSxHQUFHLEtBQUssQ0FBQztJQUMxQixDQUFDO0lB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/ams-policy.d.ts b/packages/loopover-miner/lib/ams-policy.d.ts deleted file mode 100644 index e7ee82e60a..0000000000 --- a/packages/loopover-miner/lib/ams-policy.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AmsPolicySpec } from "@loopover/engine"; -export type AmsPolicySource = "local" | "default"; -export type ResolvedAmsPolicy = { - spec: AmsPolicySpec; - source: AmsPolicySource; - warnings: string[]; -}; -export type AmsPolicyOptions = { - /** Accepted for forward/API compatibility with callers that pass a fetch override; unused today since this - * resolver never fetches (see the module doc comment above). */ - fetchImpl?: unknown; - readFileSync?: (path: string, encoding: "utf8") => string; - existsSync?: (path: string) => boolean; - env?: Record; -}; -/** Resolve the operator's local AMS policy file path: explicit env var > `LOOPOVER_MINER_CONFIG_DIR` > - * `XDG_CONFIG_HOME`/`~/.config`, mirroring every other local-store path in this package. */ -export declare function resolveAmsPolicyConfigPath(env?: Record): string; -/** - * Resolve the real, effective AMS execution policy for one attempt: the operator's own local - * `.loopover-ams.yml` when present (source: "local"), else the engine's safe defaults (source: "default"). - * Never throws -- an unreadable/malformed local file degrades through the tolerant parser to the safe - * defaults, same discipline as every other tolerant parser in this pipeline. - * - * `repoFullName` is accepted for API compatibility with callers that resolve policy per target repo, but the - * resolver intentionally does not fetch or trust target-repository AMS policy content. - */ -export declare function resolveAmsPolicy(repoFullName: string, options?: AmsPolicyOptions): Promise; diff --git a/packages/loopover-miner/lib/ams-policy.js b/packages/loopover-miner/lib/ams-policy.js deleted file mode 100644 index 0fbbfe17f9..0000000000 --- a/packages/loopover-miner/lib/ams-policy.js +++ /dev/null @@ -1,56 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { DEFAULT_AMS_POLICY_SPEC, parseAmsPolicySpecContent } from "@loopover/engine"; -import { resolveLocalStoreDbPath } from "./local-store.js"; -// Resolver for the operator-local `.loopover-ams.yml` (#5132, Wave 3.5 follow-up). AmsPolicySpec -// (ams-policy-spec.ts, engine package) is the type/parser surface; this module is the actual local -// read+resolve caller. -// -// This is deliberately NOT the same resolution shape as self-review-context.js/rejection-signal.js, which -// read from the target repo: AmsPolicySpec's fields are the OPERATOR's own execution-risk policy, so an -// untrusted target repo must never get final say over them. -const AMS_POLICY_FILENAME = ".loopover-ams.yml"; -/** Resolve the operator's local AMS policy file path: explicit env var > `LOOPOVER_MINER_CONFIG_DIR` > - * `XDG_CONFIG_HOME`/`~/.config`, mirroring every other local-store path in this package. */ -export function resolveAmsPolicyConfigPath(env = process.env) { - return resolveLocalStoreDbPath(AMS_POLICY_FILENAME, "LOOPOVER_MINER_AMS_POLICY_PATH", env); -} -function normalizeOptions(options = {}) { - return { - readFileSync: options.readFileSync ?? readFileSync, - existsSync: options.existsSync ?? existsSync, - env: options.env ?? process.env, - }; -} -/** Read the operator's own local `.loopover-ams.yml`, if one exists. Never throws: an unreadable file is - * treated the same as an absent one, falling through to the next resolution layer. */ -function readLocalAmsPolicyContent(resolved) { - const path = resolveAmsPolicyConfigPath(resolved.env); - if (!resolved.existsSync(path)) - return null; - try { - return resolved.readFileSync(path, "utf8"); - } - catch { - return null; - } -} -/** - * Resolve the real, effective AMS execution policy for one attempt: the operator's own local - * `.loopover-ams.yml` when present (source: "local"), else the engine's safe defaults (source: "default"). - * Never throws -- an unreadable/malformed local file degrades through the tolerant parser to the safe - * defaults, same discipline as every other tolerant parser in this pipeline. - * - * `repoFullName` is accepted for API compatibility with callers that resolve policy per target repo, but the - * resolver intentionally does not fetch or trust target-repository AMS policy content. - */ -export async function resolveAmsPolicy(repoFullName, options = {}) { - void repoFullName; - const resolved = normalizeOptions(options); - const localContent = readLocalAmsPolicyContent(resolved); - if (localContent !== null) { - const parsed = parseAmsPolicySpecContent(localContent); - return { spec: parsed.spec, source: "local", warnings: parsed.warnings }; - } - return { spec: DEFAULT_AMS_POLICY_SPEC, source: "default", warnings: [] }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYW1zLXBvbGljeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImFtcy1wb2xpY3kudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxZQUFZLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFFbkQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLHlCQUF5QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdEYsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFM0QsaUdBQWlHO0FBQ2pHLG1HQUFtRztBQUNuRyx1QkFBdUI7QUFDdkIsRUFBRTtBQUNGLDBHQUEwRztBQUMxRyx3R0FBd0c7QUFDeEcsNERBQTREO0FBRTVELE1BQU0sbUJBQW1CLEdBQUcsbUJBQW1CLENBQUM7QUF5QmhEOzZGQUM2RjtBQUM3RixNQUFNLFVBQVUsMEJBQTBCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDOUYsT0FBTyx1QkFBdUIsQ0FBQyxtQkFBbUIsRUFBRSxnQ0FBZ0MsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUM3RixDQUFDO0FBRUQsU0FBUyxnQkFBZ0IsQ0FBQyxVQUE0QixFQUFFO0lBQ3RELE9BQU87UUFDTCxZQUFZLEVBQUUsT0FBTyxDQUFDLFlBQVksSUFBSSxZQUFZO1FBQ2xELFVBQVUsRUFBRSxPQUFPLENBQUMsVUFBVSxJQUFJLFVBQVU7UUFDNUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxHQUFHLElBQUksT0FBTyxDQUFDLEdBQUc7S0FDaEMsQ0FBQztBQUNKLENBQUM7QUFFRDt1RkFDdUY7QUFDdkYsU0FBUyx5QkFBeUIsQ0FBQyxRQUFvQztJQUNyRSxNQUFNLElBQUksR0FBRywwQkFBMEIsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDdEQsSUFBSSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDNUMsSUFBSSxDQUFDO1FBQ0gsT0FBTyxRQUFRLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUMsQ0FBQztJQUM3QyxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FDcEMsWUFBb0IsRUFDcEIsVUFBNEIsRUFBRTtJQUU5QixLQUFLLFlBQVksQ0FBQztJQUNsQixNQUFNLFFBQVEsR0FBRyxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUUzQyxNQUFNLFlBQVksR0FBRyx5QkFBeUIsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN6RCxJQUFJLFlBQVksS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUMxQixNQUFNLE1BQU0sR0FBRyx5QkFBeUIsQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUN2RCxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDO0lBQzNFLENBQUM7SUFFRCxPQUFPLEVBQUUsSUFBSSxFQUFFLHVCQUF1QixFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxDQUFDO0FBQzVFLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/attempt-cli.d.ts b/packages/loopover-miner/lib/attempt-cli.d.ts deleted file mode 100644 index da9eda8923..0000000000 --- a/packages/loopover-miner/lib/attempt-cli.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { CodingAgentExecutionMode, FeasibilityVerdict, LocalWriteActionSpec } from "@loopover/engine"; -import type { ClaimLedger } from "./claim-ledger.js"; -import type { ClaimConflictResult, resolveClaimConflict as ResolveClaimConflictFn } from "./claim-conflict-resolver.js"; -import type { EventLedger } from "./event-ledger.js"; -import type { AttemptLog } from "./attempt-log.js"; -import type { GovernorLedger } from "./governor-ledger.js"; -import type { WorktreeAllocator } from "./worktree-allocator.js"; -import type { resolveRejectionSignaled as ResolveRejectionSignaledFn } from "./rejection-signal.js"; -import type { cleanupAttemptWorktree as CleanupAttemptWorktreeFn, prepareAttemptWorktree as PrepareAttemptWorktreeFn } from "./attempt-worktree.js"; -import type { SelfReviewContextFetch, fetchSelfReviewContext as FetchSelfReviewContextFn } from "./self-review-context.js"; -import type { buildCodingTaskSpec as BuildCodingTaskSpecFn } from "./coding-task-spec.js"; -import type { resolveAmsPolicy as ResolveAmsPolicyFn } from "./ams-policy.js"; -import type { checkMinerKillSwitch as CheckMinerKillSwitchFn } from "./governor-kill-switch.js"; -import type { getAttemptHistory as GetAttemptHistoryFn } from "./portfolio-queue.js"; -import type { recordOwnSubmission as RecordOwnSubmissionFn } from "./governor-state.js"; -import type { AttemptDeps, AttemptResult as RunMinerAttemptResult, runMinerAttempt as RunMinerAttemptFn } from "./attempt-runner.js"; -import type { submitSoftClaim as SubmitSoftClaimFn } from "./discovery-index-client.js"; -import type { resolveMinerGoalSpec as ResolveMinerGoalSpecFn } from "./miner-goal-spec.js"; -type CommonAttemptResultFields = { - repoFullName: string; - issueNumber: number; - minerLogin: string; - base: string; - mode: CodingAgentExecutionMode; - attemptId: string; -}; -/** The result runAttempt reports at every real return point, threaded to `options.onResult` (in addition to - * the plain exit-code return runAttempt itself still returns, unchanged, so bin/loopover-miner.js's own - * `process.exit(exitCode)` usage never breaks) -- the loop orchestrator's real caller for this data. */ -export type AttemptCliResult = (CommonAttemptResultFields & { - outcome: "dry_run"; -}) | (CommonAttemptResultFields & { - outcome: "blocked_rejection_signaled"; - reason: string; -}) | (CommonAttemptResultFields & { - outcome: "blocked_worktree_preparation_failed"; - reason: string; -}) | (CommonAttemptResultFields & { - outcome: "blocked_infeasible"; - reason: string; - verdict: FeasibilityVerdict; - avoidReasons: string[]; - raiseReasons: string[]; -}) | (CommonAttemptResultFields & { - outcome: `attempt_${RunMinerAttemptResult["outcome"]}`; - submissionMode: "observe" | "enforce"; - totalTurnsUsed: number; - totalCostUsd: number; - totalTokensUsed: number; - iterationsUsed: number; - reason?: string; - decision?: unknown; - spec?: LocalWriteActionSpec; - execResult?: unknown; - claimConflict?: ClaimConflictResult; -}); -export type ParsedAttemptArgs = { - error: string; -} | { - repoFullName: string; - issueNumber: number; - minerLogin: string; - base: string; - live: boolean; - dryRun: boolean; - json: boolean; -}; -export type RunAttemptOptions = { - env?: Record; - nowMs?: number; - attemptId?: string; - resolveCodingAgentModeFromConfig?: (config: { - env?: Record; - }) => CodingAgentExecutionMode; - openWorktreeAllocator?: () => WorktreeAllocator; - openClaimLedger?: () => ClaimLedger; - initEventLedger?: () => EventLedger; - initAttemptLog?: () => AttemptLog; - initGovernorLedger?: () => GovernorLedger; - buildAttemptDeps?: typeof buildAttemptDeps; - resolveRejectionSignaled?: typeof ResolveRejectionSignaledFn; - fetchImpl?: SelfReviewContextFetch; - prepareAttemptWorktree?: typeof PrepareAttemptWorktreeFn; - cleanupAttemptWorktree?: typeof CleanupAttemptWorktreeFn; - fetchSelfReviewContext?: typeof FetchSelfReviewContextFn; - buildCodingTaskSpec?: typeof BuildCodingTaskSpecFn; - resolveAmsPolicy?: typeof ResolveAmsPolicyFn; - checkMinerKillSwitch?: typeof CheckMinerKillSwitchFn; - resolveMinerGoalSpec?: typeof ResolveMinerGoalSpecFn; - runMinerAttempt?: typeof RunMinerAttemptFn; - resolveClaimConflict?: typeof ResolveClaimConflictFn; - recordOwnSubmission?: typeof RecordOwnSubmissionFn; - getAttemptHistory?: typeof GetAttemptHistoryFn; - /** Hosted soft-claim coordination at work-start/work-end, when the plane is enabled (#7168). Defaults to - * discovery-index-client.js's own submitSoftClaim. */ - submitSoftClaim?: typeof SubmitSoftClaimFn; - /** Invoked with the real structured result at every return point, in addition to (never instead of) the - * plain exit-code return -- the loop orchestrator's real hook into what actually happened. */ - onResult?: (result: AttemptCliResult) => void; -}; -export declare function parseAttemptArgs(args: string[]): ParsedAttemptArgs; -/** - * Assemble a real AttemptDeps object: every field wired to a genuine implementation (the #5131 driver, the - * #5133 slop assessor, the four real ledgers passed in, and the fetchLiveIssueSnapshot/executeLocalWrite - * built alongside this file). Throws if the coding-agent driver is unconfigured (fails closed, matching - * constructProductionCodingAgentDriver's own contract) -- callers should report that clearly rather than - * silently falling back to a driver that could never run. - */ -export declare function buildAttemptDeps(env: Record, ledgers: { - claimLedger: ClaimLedger; - eventLedger: EventLedger; - attemptLog: AttemptLog; - governorLedger: GovernorLedger; - nowMs: number; -}): AttemptDeps; -/** - * Run the `attempt` CLI subcommand end to end: resolveRejectionSignaled (before consuming a worktree slot) -> - * acquire a concurrency slot -> assemble real AttemptDeps -> prepare a REAL git worktree -> fetch a real - * SelfReviewContext -> build a real coding-task spec (blocks on an infeasible verdict) -> resolve the real - * AmsPolicySpec execution policy -> assemble the real IterateLoopInput + Governor context -> call - * runMinerAttempt for real. The worktree is cleaned up (or retained, per the real outcome) in `finally`. - * See this file's header for the documented gaps (real convergence history). - */ -export declare function runAttempt(args: string[], options?: RunAttemptOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/attempt-cli.js b/packages/loopover-miner/lib/attempt-cli.js deleted file mode 100644 index 495a16a3d2..0000000000 --- a/packages/loopover-miner/lib/attempt-cli.js +++ /dev/null @@ -1,698 +0,0 @@ -// CLI dispatch for the real attempt pipeline (#5132, Wave 3.5 -- the final assembly). Wires bin/loopover-miner.js's -// `attempt` subcommand to real infrastructure end to end: worktree allocation + real git preparation -// (worktree-allocator.js + attempt-worktree.js), the four ledgers (claim/event/attempt-log/governor), the -// real coding-agent driver (#5131) and slop assessor (#5133), a live SelfReviewContext fetch (#5145), a real -// coding-task spec (#5239), the operator's AmsPolicySpec execution policy (#5249), rejectionSignaled (#5241), -// a real runMinerAttempt call -- the first point in this epic where a real coding agent actually runs, not -// just checks-and-reports-blocked -- and, only on a real "submitted" outcome, a real post-submission -// claim-conflict resolution (#4848, claim-conflict-resolver.js) for the narrow race window -// checkSubmissionFreshness cannot see (two miners submitting almost simultaneously). -// -// KNOWN, DOCUMENTED GAPS (not fabricated -- see attempt-input-builder.js's own header for the full list): -// governor.selfPlagiarismCandidate/selfPlagiarismRecentSubmissions are omitted (chokepoint.ts's own design treats -// that as "skip that stage entirely"). governor.convergenceInput is now a real per-issue portfolio-queue.js read -// (#5654) and governor.reputationHistory a real per-repo governor-state.js read (#5675), not placeholders. -import { fingerprintFromChangedFiles, resolveCodingAgentModeFromConfig, resolveFirstConfiguredCodingAgentDriverName } from "@loopover/engine"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { constructProductionCodingAgentDriver } from "./coding-agent-construction.js"; -import { runSlopAssessment } from "./slop-assessment.js"; -import { fetchLiveIssueSnapshot } from "./live-issue-snapshot.js"; -import { executeLocalWrite } from "./execute-local-write.js"; -import { openClaimLedger } from "./claim-ledger.js"; -import { resolveMinerGoalSpec } from "./miner-goal-spec.js"; -import { resolveClaimConflict } from "./claim-conflict-resolver.js"; -import { parsePrNumberFromExecResult } from "./pr-number-parse.js"; -import { initEventLedger } from "./event-ledger.js"; -import { initAttemptLog } from "./attempt-log.js"; -import { initGovernorLedger } from "./governor-ledger.js"; -import { openWorktreeAllocator } from "./worktree-allocator.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -import { REJECTION_REASON_AI_USAGE_POLICY_BAN, REJECTION_REASON_OWN_SUBMISSION_REJECTED, resolveRejectionSignaled } from "./rejection-signal.js"; -import { cleanupAttemptWorktree, prepareAttemptWorktree } from "./attempt-worktree.js"; -import { fetchSelfReviewContext } from "./self-review-context.js"; -import { buildCodingTaskSpec } from "./coding-task-spec.js"; -import { resolveAmsPolicy } from "./ams-policy.js"; -import { checkMinerKillSwitch, recordMinerKillSwitchTransition } from "./governor-kill-switch.js"; -import { captureMinerError } from "./sentry.js"; -import { buildAttemptGovernorContext, buildAttemptLoopInput } from "./attempt-input-builder.js"; -import { getAttemptHistory } from "./portfolio-queue.js"; -import { loadReputationHistory, recordOwnSubmission } from "./governor-state.js"; -import { runMinerAttempt } from "./attempt-runner.js"; -import { resolveGitHubToken } from "./github-token-resolution.js"; -import { isDiscoveryPlaneEnabled, submitSoftClaim } from "./discovery-index-client.js"; -const ATTEMPT_USAGE = "Usage: loopover-miner attempt --miner-login [--base ] [--live] [--dry-run] [--json]"; -function parseRepoTarget(value) { - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - return null; - return `${owner}/${repo}`; -} -export function parseAttemptArgs(args) { - const options = { json: false, minerLogin: null, base: "main", live: false, dryRun: false }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // Opt-in only: resolveCodingAgentModeFromConfig's own default (no agentDryRun override) is "live", not - // "dry_run" -- so #5132's "dry-run is default" acceptance criteria (#2342) has to be enforced HERE, by - // requiring an explicit --live flag before this command will ever request live mode. - if (token === "--live") { - options.live = true; - continue; - } - // #4847: distinct from --live's absence above -- --live only ever gated the coding-agent DRIVER's mode, - // but a non---live run still opened every store and made real worktree/claim/ledger writes. --dry-run - // short-circuits BEFORE any of that infrastructure is even opened, guaranteeing zero writes rather than - // merely skipping the driver. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--miner-login") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: ATTEMPT_USAGE }; - options.minerLogin = value; - index += 1; - continue; - } - if (token === "--base") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: ATTEMPT_USAGE }; - options.base = value; - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - positional.push(token); - } - if (positional.length !== 2) - return { error: ATTEMPT_USAGE }; - const repoFullName = parseRepoTarget(positional[0]); - if (!repoFullName) - return { error: `Repository must be in owner/repo form: ${positional[0]}` }; - const issueNumber = Number(positional[1]); - if (!Number.isInteger(issueNumber) || issueNumber < 1) { - return { error: `Issue number must be a positive integer: ${positional[1]}` }; - } - if (!options.minerLogin) - return { error: `--miner-login is required. ${ATTEMPT_USAGE}` }; - return { - repoFullName, - issueNumber, - minerLogin: options.minerLogin, - base: options.base, - live: options.live, - dryRun: options.dryRun, - json: options.json, - }; -} -/** - * Assemble a real AttemptDeps object: every field wired to a genuine implementation (the #5131 driver, the - * #5133 slop assessor, the four real ledgers passed in, and the fetchLiveIssueSnapshot/executeLocalWrite - * built alongside this file). Throws if the coding-agent driver is unconfigured (fails closed, matching - * constructProductionCodingAgentDriver's own contract) -- callers should report that clearly rather than - * silently falling back to a driver that could never run. - */ -export function buildAttemptDeps(env, ledgers) { - // AttemptDeps' claimLedger/callback parameter types are looser structural stubs than the real ledgers - // (pre-existing .d.ts drift on attempt-runner); cast preserves the same runtime wiring the .js had. - return { - driver: constructProductionCodingAgentDriver(env), - runSlopAssessment: (input) => runSlopAssessment(input), - appendAttemptLogEvent: (event) => { - ledgers.attemptLog.appendAttemptLogEvent(event); - }, - claimLedger: ledgers.claimLedger, - // resolveGitHubToken (#6116): GITHUB_TOKEN env override wins outright, else a live token from the - // authenticated `loopover-mcp login` session -- cached in memory, so repeat calls within this process - // don't repeatedly hit the session-fetch endpoint after the first successful resolution. - fetchLiveIssueSnapshot: async (repoFullName, issueNumber) => { - // resolveGitHubToken returns string | null; exactOptionalPropertyTypes forbids explicit undefined. - const githubToken = await resolveGitHubToken(env); - return fetchLiveIssueSnapshot(repoFullName, issueNumber, githubToken !== null ? { githubToken } : {}); - }, - eventLedger: ledgers.eventLedger, - governorLedgerAppend: (event) => ledgers.governorLedger.appendGovernorEvent(event), - nowMs: ledgers.nowMs, - executeLocalWrite: (spec) => executeLocalWrite(spec), - }; -} -/** - * Run the `attempt` CLI subcommand end to end: resolveRejectionSignaled (before consuming a worktree slot) -> - * acquire a concurrency slot -> assemble real AttemptDeps -> prepare a REAL git worktree -> fetch a real - * SelfReviewContext -> build a real coding-task spec (blocks on an infeasible verdict) -> resolve the real - * AmsPolicySpec execution policy -> assemble the real IterateLoopInput + Governor context -> call - * runMinerAttempt for real. The worktree is cleaned up (or retained, per the real outcome) in `finally`. - * See this file's header for the documented gaps (real convergence history). - */ -export async function runAttempt(args, options = {}) { - const parsed = parseAttemptArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const env = options.env ?? process.env; - const nowMs = options.nowMs ?? Date.now(); - const resolveMode = options.resolveCodingAgentModeFromConfig ?? resolveCodingAgentModeFromConfig; - // resolveCodingAgentModeFromConfig accepts agentDryRun at runtime; RunAttemptOptions injectable omits it (.d.ts drift). - const mode = resolveMode({ env, agentDryRun: !parsed.live }); - if (mode === "paused") { - return reportCliFailure(parsed.json, `Coding-agent execution is globally paused (MINER_CODING_AGENT_PAUSED). Not running attempt for ${parsed.repoFullName}#${parsed.issueNumber}.`, 3); - } - const attemptId = options.attemptId ?? `${parsed.repoFullName.replace("/", "_")}-${parsed.issueNumber}-${nowMs}`; - // #4847: reports what a real run would do and returns BEFORE any store (allocator/claim/event/attempt-log/ - // governor ledger) is even opened, so this is a provable zero-write path -- not just "opened but didn't - // write to" the local stores, and nowhere near the real worktree clone, claim, or coding-agent driver. - if (parsed.dryRun) { - const dryRunResult = { - outcome: "dry_run", - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would attempt ${parsed.repoFullName}#${parsed.issueNumber} for ${parsed.minerLogin} (mode: ${mode}, base: ${parsed.base}). No worktree, claim, or ledger writes were made.`); - } - options.onResult?.(dryRunResult); - return 0; - } - let allocator = null; - let claimLedger = null; - let eventLedger = null; - let attemptLog = null; - let governorLedger = null; - let allocation = null; - let worktreeResult = null; - let claimedIssue = false; - let claimRecord = null; - try { - allocator = (options.openWorktreeAllocator ?? openWorktreeAllocator)(); - claimLedger = (options.openClaimLedger ?? openClaimLedger)(); - eventLedger = (options.initEventLedger ?? initEventLedger)(); - attemptLog = (options.initAttemptLog ?? initAttemptLog)(); - governorLedger = (options.initGovernorLedger ?? initGovernorLedger)(); - // Checked before acquiring a worktree slot: a rejection-signaled repo should never consume one. - // resolveRejectionSignaled resolves both documented triggers (#5132 policy ban, #5655 own-rejection - // history) and returns a trigger-specific reason string for accurate audit-trail labeling. - const resolveRejection = options.resolveRejectionSignaled ?? resolveRejectionSignaled; - // Pass fetchImpl through even when unset (same shape the .js always produced); cast for - // exactOptionalPropertyTypes vs RejectionSignaledOptions (pre-existing optional-prop drift). - const rejectionSignal = await resolveRejection(parsed.repoFullName, { - fetchImpl: options.fetchImpl, - }); - if (rejectionSignal) { - const reason = rejectionSignal === true ? REJECTION_REASON_AI_USAGE_POLICY_BAN : rejectionSignal; - attemptLog.appendAttemptLogEvent({ - eventType: "attempt_aborted", - attemptId, - actionClass: "open_pr", - mode, - reason, - payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }, - }); - eventLedger.appendEvent({ - type: "attempt_blocked", - repoFullName: parsed.repoFullName, - payload: { issueNumber: parsed.issueNumber, reason }, - }); - const rejectedResult = { - outcome: "blocked_rejection_signaled", - reason, - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - }; - if (parsed.json) { - console.log(JSON.stringify(rejectedResult, null, 2)); - } - else { - console.error(reason === REJECTION_REASON_OWN_SUBMISSION_REJECTED - ? `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this miner was previously rejected on this repo.` - : `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's AI-usage policy bans automated/AI-authored contributions.`); - } - options.onResult?.(rejectedResult); - return 5; - } - allocation = allocator.acquire(attemptId, parsed.repoFullName); - let deps; - try { - const buildDeps = options.buildAttemptDeps ?? buildAttemptDeps; - deps = buildDeps(env, { claimLedger, eventLedger, attemptLog, governorLedger, nowMs }); - } - catch (error) { - const reason = describeCliError(error); - return reportCliFailure(parsed.json, `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: ${reason}`, 3); - } - // Real worktree preparation (repo-clone.js + attempt-worktree.js, #5237): the allocator above only - // reserves a concurrency SLOT (worktree-allocator.js's own `slot-N` placeholder dirs never receive real - // git content) -- this is the step that actually clones/fetches the target repo and creates a real - // `git worktree` for this attempt. Its own path, NOT the allocator's slot path, is the real - // workingDirectory a future runMinerAttempt call must use. - const prepareWorktree = options.prepareAttemptWorktree ?? prepareAttemptWorktree; - worktreeResult = await prepareWorktree(parsed.repoFullName, attemptId, { baseBranch: parsed.base, env }); - if (!worktreeResult.ok) { - const reason = worktreeResult.error; - attemptLog.appendAttemptLogEvent({ - eventType: "attempt_aborted", - attemptId, - actionClass: "open_pr", - mode, - reason, - payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }, - }); - eventLedger.appendEvent({ - type: "attempt_blocked", - repoFullName: parsed.repoFullName, - payload: { issueNumber: parsed.issueNumber, reason }, - }); - const worktreeFailureResult = { - outcome: "blocked_worktree_preparation_failed", - reason, - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - }; - if (parsed.json) { - console.log(JSON.stringify(worktreeFailureResult, null, 2)); - } - else { - console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: real worktree preparation failed: ${reason}`); - } - options.onResult?.(worktreeFailureResult); - return 6; - } - // Real SelfReviewContext (#5145): issue/PR/manifest data at live-gate fidelity for the target repo. - const fetchReviewContext = options.fetchSelfReviewContext ?? fetchSelfReviewContext; - const reviewGithubToken = await resolveGitHubToken(env); - const reviewContext = await fetchReviewContext(parsed.repoFullName, { - ...(reviewGithubToken !== null ? { githubToken: reviewGithubToken } : {}), - contributorLogin: parsed.minerLogin, - linkedIssues: [parsed.issueNumber], - }); - // The target issue's own real record, when present in the fetched context. When absent (e.g. already - // closed, or genuinely not found), buildCodingTaskSpec's own feasibility check reports target_not_found - // and this placeholder's empty title/body are never surfaced anywhere -- not fabricated content, just an - // inert shape for a verdict that immediately blocks. - const targetIssue = reviewContext.issues.find((candidate) => candidate.number === parsed.issueNumber) ?? { - number: parsed.issueNumber, - title: "", - body: null, - labels: [], - }; - const buildTaskSpec = options.buildCodingTaskSpec ?? buildCodingTaskSpec; - // CodingTaskClaimLedger's listClaims filter types status as plain string (pre-existing .d.ts drift). - const codingTaskSpec = buildTaskSpec({ - repoFullName: parsed.repoFullName, - issue: targetIssue, - context: { issues: reviewContext.issues, pullRequests: reviewContext.pullRequests }, - claimLedger: claimLedger, - workingDirectory: worktreeResult.worktreePath, - }); - if (!codingTaskSpec.ready) { - const reason = `infeasible_${codingTaskSpec.verdict}`; - attemptLog.appendAttemptLogEvent({ - eventType: "attempt_aborted", - attemptId, - actionClass: "open_pr", - mode, - reason, - payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, feasibility: codingTaskSpec.feasibility }, - }); - eventLedger.appendEvent({ - type: "attempt_blocked", - repoFullName: parsed.repoFullName, - payload: { issueNumber: parsed.issueNumber, reason }, - }); - const infeasibleResult = { - outcome: "blocked_infeasible", - reason, - verdict: codingTaskSpec.verdict, - avoidReasons: codingTaskSpec.feasibility.avoidReasons, - raiseReasons: codingTaskSpec.feasibility.raiseReasons, - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - }; - if (parsed.json) { - console.log(JSON.stringify(infeasibleResult, null, 2)); - } - else { - console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: feasibility verdict "${codingTaskSpec.verdict}" (${[...codingTaskSpec.feasibility.avoidReasons, ...codingTaskSpec.feasibility.raiseReasons].join(", ")}).`); - } - options.onResult?.(infeasibleResult); - return 4; - } - const amsPolicy = await (options.resolveAmsPolicy ?? resolveAmsPolicy)(parsed.repoFullName, { env }); - // Real per-repo pause (#5392): read straight from the already-cloned worktree's own .loopover-miner.yml - // (resolveMinerGoalSpec never throws -- a missing/malformed file degrades to killSwitch.paused: false, so - // this can't fail this attempt on its own). Threaded into BOTH checkMinerKillSwitch (killSwitchScope, used - // by the freshness/submission gate) and the governor context (killSwitchRepoPaused, used by the Governor - // chokepoint) -- the same two places the GLOBAL kill switch already reaches. - const resolveGoalSpec = options.resolveMinerGoalSpec ?? resolveMinerGoalSpec; - const minerGoalSpec = resolveGoalSpec(worktreeResult.repoPath); - const repoPaused = minerGoalSpec.spec.killSwitch.paused; - const checkKillSwitch = options.checkMinerKillSwitch ?? checkMinerKillSwitch; - // recordMinerKillSwitchTransition is used at runtime but omitted from RunAttemptOptions (.d.ts drift). - const recordKillTransition = options - .recordMinerKillSwitchTransition ?? recordMinerKillSwitchTransition; - let killSwitchScope = checkKillSwitch({ env, repoPaused }).scope; - let previousKillSwitchScope = killSwitchScope; - // Captured after the ok-check above so the mid-attempt kill-switch probe can't see a null worktreeResult. - const preparedWorktree = worktreeResult; - const resolveLiveKillSwitch = () => { - // Re-read the YAML flag each probe so an on-disk unpause/pause is reflected mid-attempt (#5670). - const liveRepoPaused = resolveGoalSpec(preparedWorktree.repoPath).spec.killSwitch.paused; - const live = checkKillSwitch({ env, repoPaused: liveRepoPaused }); - if (live.scope !== previousKillSwitchScope) { - try { - recordKillTransition({ - repoFullName: parsed.repoFullName, - actionClass: "attempt", - previousScope: previousKillSwitchScope, - scope: live.scope, - }); - } - catch (error) { - // Ledger append must never crash an aborting attempt (kept), but was previously silent -- a - // kill-switch flip mid-attempt (a compliance-relevant event) could vanish with no record (#6011). - captureMinerError(error, { kind: "kill_switch_transition_record_failed", repoFullName: parsed.repoFullName, scope: live.scope }); - } - previousKillSwitchScope = live.scope; - } - killSwitchScope = live.scope; - return live; - }; - const shouldAbort = () => { - const live = resolveLiveKillSwitch(); - if (!live.active) - return false; - return { - abort: true, - reason: `Kill-switch (${live.scope}) engaged mid-attempt; abandoning without starting another driver iteration.`, - }; - }; - const loopInput = buildAttemptLoopInput({ - codingTaskSpec, - reviewContext, - worktreePath: worktreeResult.worktreePath, - attemptId, - mode, - repoFullName: parsed.repoFullName, - minerLogin: parsed.minerLogin, - rejectionSignaled: false, - amsPolicySpec: amsPolicy.spec, - branchRef: worktreeResult.branchName, - }); - // Real per-issue attempt history (#5654): portfolio-queue.js's own claim/reclaim/requeue/done counters, - // keyed the same way opportunity-fanout.js enqueues issue-shaped candidates (`issue:`). No - // apiBaseUrl: this file has no multi-forge host context of its own today, so this reads (and every - // pre-#5563 single-forge caller already reads) the github.com default. - const readAttemptHistory = options.getAttemptHistory ?? getAttemptHistory; - const convergenceInput = readAttemptHistory(parsed.repoFullName, `issue:${parsed.issueNumber}`); - // Real per-repo reputation history (#5675): the miner's own decided/unfavorable outcome streak for this repo, - // read from governor-state.js so the chokepoint's self-reputation throttle sees real data instead of nothing. - // loadReputationHistory is used at runtime but omitted from RunAttemptOptions (.d.ts drift). - const readReputationHistory = options.loadReputationHistory ?? - loadReputationHistory; - const reputationHistory = readReputationHistory(parsed.repoFullName); - const governor = buildAttemptGovernorContext(env, amsPolicy.spec, repoPaused, convergenceInput, reputationHistory); - // Real maxConcurrentClaims enforcement (#6758): the repo's .loopover-miner.yml cap is honored ATOMICALLY by - // the ledger's count-and-claim, not by a listActiveClaims pre-check here. The old check-then-act split -- read - // the count in this file, then record the claim in a separate claimLedger call -- let two sibling miner - // processes racing the same repo both pass a stale sub-cap count and both claim, exceeding the cap. - // claimIssueWithinCap fuses the count and the insert into one transaction; the loser gets `claimed: false` - // and is reported below rather than silently dropped. This is also the real soft-claim (#5393): once it - // returns claimed, a sibling process sees it via claimLedger.listActiveClaims while this attempt is in - // flight, it is released in `finally` on every terminal outcome (mirroring the worktree allocation slot's - // acquire-then-always-release), and its claimedAt feeds the post-submission conflict check further down (#4848). - const claimResult = claimLedger.claimIssueWithinCap(parsed.repoFullName, parsed.issueNumber, `attempt:${attemptId}`, undefined, minerGoalSpec.spec.maxConcurrentClaims); - if (!claimResult.claimed) { - const reason = "max_concurrent_claims_exceeded"; - attemptLog.appendAttemptLogEvent({ - eventType: "attempt_aborted", - attemptId, - actionClass: "open_pr", - mode, - reason, - payload: { - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - maxConcurrentClaims: minerGoalSpec.spec.maxConcurrentClaims, - activeClaimCount: claimResult.activeClaimCount, - }, - }); - eventLedger.appendEvent({ - type: "attempt_blocked", - repoFullName: parsed.repoFullName, - payload: { issueNumber: parsed.issueNumber, reason }, - }); - const blockedResult = { - outcome: "blocked_max_concurrent_claims", - reason, - maxConcurrentClaims: minerGoalSpec.spec.maxConcurrentClaims, - activeClaimCount: claimResult.activeClaimCount, - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - }; - if (parsed.json) { - console.log(JSON.stringify(blockedResult, null, 2)); - } - else { - console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's maxConcurrentClaims cap (${minerGoalSpec.spec.maxConcurrentClaims}) is already met (${claimResult.activeClaimCount} active claim(s)).`); - } - // blocked_max_concurrent_claims is a real runtime outcome omitted from AttemptCliResult (.d.ts drift). - options.onResult?.(blockedResult); - return 11; - } - claimRecord = claimResult.claim; - claimedIssue = true; - // Hosted soft-claim coordination (#7168), opt-in via LOOPOVER_MINER_DISCOVERY_PLANE -- gated HERE at the - // call site (not left to submitSoftClaim's own internal check alone) so a disabled plane costs zero calls, - // matching discover-cli.js's supplementWithDiscoveryIndex gating; a caller-injected options.submitSoftClaim - // (tests, or a future programmatic caller) can't accidentally bypass the opt-in this way either. Awaited - // (not fire-and-forget) so a sibling instance racing the same issue is genuinely less likely to start - // duplicate work in the window before this attempt's claim reaches the shared index -- the whole point of - // coordinating BEFORE work begins, not after. - if (isDiscoveryPlaneEnabled(env)) { - const submitClaim = options.submitSoftClaim ?? submitSoftClaim; - await submitClaim(claimRecord, { env }); - } - const runAttemptPipeline = options.runMinerAttempt ?? runMinerAttempt; - let result; - try { - result = await runAttemptPipeline({ - loopInput, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - killSwitchScope, - slopThreshold: amsPolicy.spec.slopThreshold, - submissionMode: amsPolicy.spec.submissionMode, - governor, - }, { - ...deps, - shouldAbort, - resolveKillSwitchScope: () => resolveLiveKillSwitch().scope, - }); - } - catch (error) { - // A real attempt that CRASHED is exactly the case that most needs its worktree kept for post-mortem - // inspection, so record the failure explicitly before unwinding. Without this, `attemptOk` stayed - // `undefined` and the finally block's `?? true` default (meant for the earlier blocked paths that never - // ran anything in the worktree) deleted it -- inverting shouldRetainWorktree's documented policy. - worktreeResult.attemptOk = false; - throw error; - } - worktreeResult.attemptOk = result.outcome === "submitted"; - // Real claim-conflict resolution (#4848): only meaningful once a real PR exists, so this only ever runs - // on a real "submitted" outcome. checkSubmissionFreshness (inside runMinerAttempt) already caught the - // common pre-submission case; this closes the narrower TOCTOU window where two miners raced past that - // check almost simultaneously -- see claim-conflict-resolver.js's own header for why the adjudicator - // can only run POST-submission (it needs a real PR number on both sides of the election). - let claimConflict; - if (result.outcome === "submitted") { - const selfPrNumber = parsePrNumberFromExecResult(result.execResult, parsed.repoFullName); - if (selfPrNumber !== null) { - const resolveConflict = options.resolveClaimConflict ?? resolveClaimConflict; - claimConflict = await resolveConflict({ - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - selfPrNumber, - selfClaimedAt: claimRecord.claimedAt, - minerLogin: parsed.minerLogin, - }, { fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, executeLocalWrite: deps.executeLocalWrite }); - } - // Real own-submission history (#5655 follow-up): governor-state.js's recordOwnSubmission/ - // listRecentOwnSubmissions store (#5134) existed and was already READ by resolveOwnRejectionHistory - // (#5655), but nothing ever WROTE to it -- attempt-runner.js's own header names this exact gap - // ("real persistence primitives... but isn't auto-loaded here yet"). Left unfixed, that trigger is a - // silent no-op in every real deployment: an empty table always resolves "no prior submissions found." - // The fingerprint is the real changed-files set from the loop's own handoff packet (never fabricated) -- - // omitted (not recorded as an empty placeholder) when the packet reports no changed files at all. A - // logging failure must never fail an otherwise-successful attempt, matching the summary-event write below. - const changedFiles = result.loopResult.handoffPacket?.changedFiles?.map((file) => file.path) ?? []; - const fingerprint = fingerprintFromChangedFiles(changedFiles); - if (fingerprint) { - try { - const record = options.recordOwnSubmission ?? recordOwnSubmission; - record({ - repoFullName: parsed.repoFullName, - fingerprint, - submittedAt: new Date(nowMs).toISOString(), - pullRequestNumber: selfPrNumber, - issueNumber: parsed.issueNumber, - }); - } - catch (error) { - // A logging failure must never fail an otherwise-successful attempt (kept), but was previously - // silent -- if this write fails AFTER a real PR has already opened, future self-plagiarism checks go - // permanently blind to this exact submission with nobody told (#6011). - captureMinerError(error, { kind: "record_own_submission_failed", repoFullName: parsed.repoFullName, pullRequestNumber: selfPrNumber }); - } - } - } - const finalResult = { - outcome: `attempt_${result.outcome}`, - repoFullName: parsed.repoFullName, - issueNumber: parsed.issueNumber, - minerLogin: parsed.minerLogin, - base: parsed.base, - mode, - attemptId, - submissionMode: amsPolicy.spec.submissionMode, - // Every runMinerAttempt outcome carries a real loopResult (#5135's loop needs its genuine turn-usage and - // cost to save real GovernorCapUsage via governor-state.js's saveCapUsage -- nothing else in the codebase - // calls it yet). Surfaced flat rather than the whole loopResult object, matching this result's own - // shallow shape. costUsd is real only for the agent-sdk provider (its own SDK result message reports - // total_cost_usd); CLI-subprocess providers (claude-cli/codex-cli) report no cost signal today, so this - // is 0 for those -- an honest absence, not a fabricated number. - totalTurnsUsed: result.loopResult.totalTurnsUsed, - totalCostUsd: result.loopResult.totalCostUsd, - // Real accumulated tokens (#5653) -- read from finalMeterTotals rather than a flat totalTokensUsed field - // (IterateLoopResult has no such flat field, unlike turns/cost). 0 when no driver reported a token signal - // on any iteration this attempt ran, never fabricated. - totalTokensUsed: result.loopResult.finalMeterTotals.tokens, - iterationsUsed: result.loopResult.iterationsUsed, - ...(result.outcome === "abandon" && result.loopResult.finalDecision?.abandonReason - ? { abandonReason: result.loopResult.finalDecision.abandonReason } - : {}), - ...("reason" in result ? { reason: result.reason } : {}), - ...("decision" in result ? { decision: result.decision } : {}), - ...("spec" in result ? { spec: result.spec } : {}), - ...("execResult" in result ? { execResult: result.execResult } : {}), - // Present only on a real "submitted" outcome whose PR number was recoverable from execResult -- omitted - // (not fabricated as "checked: false") on every other outcome, and on a submitted outcome where the new - // PR's number genuinely couldn't be parsed (an honest gap, not silently swallowed). - ...(claimConflict !== undefined ? { claimConflict } : {}), - }; - // One summary row per completed attempt (#5185), for the Grafana per-provider usage dashboard the redacted - // AMS reporting export exposes -- distinct from the per-iteration attempt_started/attempt_tool_edit/... trail - // iterate-loop.ts already writes. No fallback for an unconfigured provider: buildAttemptDeps already fails - // closed (throws) on the same env before a worktree is even allocated, so reaching this point guarantees - // resolveFirstConfiguredCodingAgentDriverName(env) resolves a real name. costUsd/tokensUsed are both real, - // driver-reported accumulated totals (#5653) -- 0 when no iteration's driver reported a signal, never - // fabricated. A logging failure must never fail an otherwise-successful attempt -- mirrors iterate-loop.ts's - // own safeAppendAttemptLogEvent non-fatal handling. - try { - attemptLog.appendAttemptLogEvent({ - eventType: "attempt_outcome_summary", - attemptId, - actionClass: finalResult.outcome, - mode, - reason: `attempt finished with outcome: ${result.outcome}`, - provider: resolveFirstConfiguredCodingAgentDriverName(env), - costUsd: finalResult.totalCostUsd, - tokensUsed: finalResult.totalTokensUsed, - }); - } - catch (error) { - // A logging failure must never fail an otherwise-successful attempt (kept), but was previously silent -- - // per docs/observability.md this row feeds the Grafana per-provider cost/usage dashboard, so a failure - // here silently drops the attempt from operator-facing metrics with nobody told (#6011). - captureMinerError(error, { kind: "attempt_outcome_summary_append_failed", attemptId, repoFullName: parsed.repoFullName }); - } - if (parsed.json) { - console.log(JSON.stringify(finalResult, null, 2)); - } - else { - console.log(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} finished with outcome: ${result.outcome}.`); - } - options.onResult?.(finalResult); - switch (result.outcome) { - case "submitted": - return 0; - case "abandon": - return 7; - case "stale": - return 8; - case "blocked": - return 9; - case "governed": - return 10; - default: - return 2; - } - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - // worktreeResult.attemptOk is set to the REAL runMinerAttempt outcome (submitted = true) once that call - // happens, and explicitly to `false` when that call THROWS -- a crashed attempt is precisely what needs a - // retained worktree to postmortem, so it must never fall through to the `?? true` default below. Every - // earlier blocked path (rejection/worktree-prep-failure/infeasible) never sets it, since nothing ran in - // the worktree to postmortem -- those are the cases that default to `true` (nothing to retain), matching - // cleanupAttemptWorktree's own retention policy (a failed REAL attempt is what gets retained). - if (worktreeResult?.ok) { - const cleanupWorktree = options.cleanupAttemptWorktree ?? cleanupAttemptWorktree; - await cleanupWorktree(worktreeResult.repoPath, worktreeResult.worktreePath, worktreeResult.attemptOk ?? true); - } - // Every terminal outcome past the claim point (submitted/abandon/stale/blocked/governed, or an - // unexpected throw) releases the soft-claim -- a claim that outlives its own attempt process would - // wrongly tell a sibling miner this issue is still in flight. - if (claimedIssue && claimLedger) - claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber); - // Paired hosted release (#7168): same call-site opt-in gate as the claim submission above. Only fires when - // the initial claim submission actually ran (claimRecord is only set once claimedIssue is), so a run that - // never reached the claim point (e.g. blocked_max_concurrent_claims) has nothing to release remotely. - if (claimedIssue && claimRecord && isDiscoveryPlaneEnabled(env)) { - const submitClaim = options.submitSoftClaim ?? submitSoftClaim; - await submitClaim({ ...claimRecord, status: "released" }, { env }); - } - if (allocation && allocator) - allocator.release(attemptId); - allocator?.close(); - claimLedger?.close(); - eventLedger?.close(); - attemptLog?.close(); - governorLedger?.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXR0ZW1wdC1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdHRlbXB0LWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxvSEFBb0g7QUFDcEgscUdBQXFHO0FBQ3JHLDBHQUEwRztBQUMxRyw2R0FBNkc7QUFDN0csOEdBQThHO0FBQzlHLDJHQUEyRztBQUMzRyxxR0FBcUc7QUFDckcsMkZBQTJGO0FBQzNGLHFGQUFxRjtBQUNyRixFQUFFO0FBQ0YsMEdBQTBHO0FBQzFHLGtIQUFrSDtBQUNsSCxpSEFBaUg7QUFDakgsMkdBQTJHO0FBRTNHLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxnQ0FBZ0MsRUFBRSwyQ0FBMkMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRTlJLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRixPQUFPLEVBQUUsb0NBQW9DLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUN0RixPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUN6RCxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUNsRSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFcEQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDNUQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFFcEUsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDbkUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXBELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUVsRCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxzQkFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/attempt-input-builder.d.ts b/packages/loopover-miner/lib/attempt-input-builder.d.ts deleted file mode 100644 index b741779513..0000000000 --- a/packages/loopover-miner/lib/attempt-input-builder.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { AmsPolicySpec, CodingAgentExecutionMode, IterateLoopInput, PortfolioConvergenceInput, RepoOutcomeHistory, SelfReviewContext } from "@loopover/engine"; -import type { AttemptGovernorContext } from "./attempt-runner.js"; -import type { CodingTaskSpecResult } from "./coding-task-spec.js"; -/** - * Assemble the real Governor chokepoint context for one attempt. rateLimitBuckets/rateLimitBackoffAttempts/ - * capUsage are deliberately omitted -- evaluateGovernorChokepointGatePersisted (#5134) auto-loads them from - * the persisted governor-state store when absent. - * - * `repoPaused` (#5392) is the caller's own resolved `MinerGoalSpec.killSwitch.paused` for the target repo - * (miner-goal-spec.js's resolveMinerGoalSpec) -- this composer stays pure and just threads whatever the - * caller already resolved through; passing nothing keeps the prior fails-open-on-that-axis-only behavior. - * - * `convergenceInput` (#5654) is the caller's own real portfolio-queue.js `getAttemptHistory` read -- this - * composer stays pure and just threads it through, same as `repoPaused`. Omitted (never fabricated) falls - * back to the honest first-attempt-shaped zero-state, so a caller that hasn't wired a real read yet (or an - * item genuinely absent from the queue) still produces a well-formed `PortfolioConvergenceInput`. - * - * `reputationHistory` (#5675) is the caller's own real governor-state.js `loadReputationHistory` read for the - * target repo. Optional and threaded through unchanged: when omitted the field is left off entirely, which - * chokepoint.ts treats as "skip the self-reputation throttle" -- an honest absence, never a fabricated clean - * history. - */ -export declare function buildAttemptGovernorContext(env: Record, amsPolicySpec: AmsPolicySpec, repoPaused?: boolean, convergenceInput?: PortfolioConvergenceInput, reputationHistory?: RepoOutcomeHistory): AttemptGovernorContext; -export type BuildAttemptLoopInputInput = { - codingTaskSpec: Extract; - reviewContext: SelfReviewContext; - worktreePath: string; - attemptId: string; - mode: CodingAgentExecutionMode; - repoFullName: string; - minerLogin: string; - rejectionSignaled: boolean; - amsPolicySpec: AmsPolicySpec; - branchRef?: string; -}; -/** - * Assemble the real IterateLoopInput for one attempt from every already-computed real dependency. Pure -- - * throws nothing itself (callers are expected to have already validated `codingTaskSpec.ready`). - */ -export declare function buildAttemptLoopInput(input: BuildAttemptLoopInputInput): IterateLoopInput; diff --git a/packages/loopover-miner/lib/attempt-input-builder.js b/packages/loopover-miner/lib/attempt-input-builder.js deleted file mode 100644 index 117e612f9e..0000000000 --- a/packages/loopover-miner/lib/attempt-input-builder.js +++ /dev/null @@ -1,83 +0,0 @@ -import { isGlobalMinerKillSwitch, isGlobalMinerLiveModeOptIn } from "@loopover/engine"; -// Pure composers for runMinerAttempt's real input (#5132, Wave 3.5 -- the final assembly). Everything here is -// a plain in/out transform over already-fetched/already-computed real data (coding-task-spec, #5239; -// self-review-context, #5145; worktree preparation, #5237/#5252; AmsPolicySpec, #5249) -- no fetching, no IO, -// same discipline as coding-task-spec.js's own composers. -// -// KNOWN, DOCUMENTED GAPS (not fabricated -- explicitly left as real, narrow follow-ups): -// - governor.selfPlagiarismCandidate/selfPlagiarismRecentSubmissions are omitted, which chokepoint.ts's own -// design treats as "skip that stage entirely" -- an honest absence, not a fabricated "clean" verdict. -// -// governor.convergenceInput is now a REAL per-issue attempt-history query (#5654) and governor.reputationHistory -// a REAL per-repo governor-state query (#5675): the caller (attempt-cli.js) reads them from portfolio-queue.js's -// getAttemptHistory and governor-state.js's loadReputationHistory and passes them in here, this composer staying -// pure over them same as every other already-computed dependency below. An omitted argument stays an honest -// absence (zero-state convergence / skipped reputation throttle), never a fabricated clean history. -/** - * Assemble the real Governor chokepoint context for one attempt. rateLimitBuckets/rateLimitBackoffAttempts/ - * capUsage are deliberately omitted -- evaluateGovernorChokepointGatePersisted (#5134) auto-loads them from - * the persisted governor-state store when absent. - * - * `repoPaused` (#5392) is the caller's own resolved `MinerGoalSpec.killSwitch.paused` for the target repo - * (miner-goal-spec.js's resolveMinerGoalSpec) -- this composer stays pure and just threads whatever the - * caller already resolved through; passing nothing keeps the prior fails-open-on-that-axis-only behavior. - * - * `convergenceInput` (#5654) is the caller's own real portfolio-queue.js `getAttemptHistory` read -- this - * composer stays pure and just threads it through, same as `repoPaused`. Omitted (never fabricated) falls - * back to the honest first-attempt-shaped zero-state, so a caller that hasn't wired a real read yet (or an - * item genuinely absent from the queue) still produces a well-formed `PortfolioConvergenceInput`. - * - * `reputationHistory` (#5675) is the caller's own real governor-state.js `loadReputationHistory` read for the - * target repo. Optional and threaded through unchanged: when omitted the field is left off entirely, which - * chokepoint.ts treats as "skip the self-reputation throttle" -- an honest absence, never a fabricated clean - * history. - */ -export function buildAttemptGovernorContext(env, amsPolicySpec, repoPaused, convergenceInput, reputationHistory) { - return { - killSwitchGlobal: isGlobalMinerKillSwitch(env), - killSwitchRepoPaused: repoPaused, - liveModeGlobalOptIn: isGlobalMinerLiveModeOptIn(env), - capLimits: amsPolicySpec.capLimits, - convergenceInput: convergenceInput ?? { attempts: 0, consecutiveFailures: 0, reenqueues: 0, reachedDone: false }, - ...(reputationHistory === undefined ? {} : { reputationHistory }), - }; -} -/** - * Assemble the real IterateLoopInput for one attempt from every already-computed real dependency. Pure -- - * throws nothing itself (callers are expected to have already validated `codingTaskSpec.ready`). - */ -export function buildAttemptLoopInput(input) { - return { - attemptId: input.attemptId, - workingDirectory: input.worktreePath, - acceptanceCriteriaPath: input.codingTaskSpec.acceptanceCriteriaPath, - instructions: input.codingTaskSpec.instructions, - mode: input.mode, - maxIterations: input.amsPolicySpec.maxIterations, - maxTurnsPerIteration: input.amsPolicySpec.maxTurnsPerIteration, - // Real mid-attempt budget (#5395): the SAME Governor cap ceilings that already bound cross-cycle spend - // (loop-cli.js's after-the-fact governorState.saveCapUsage) now also bound this ONE attempt in progress, - // via the engine's real accumulateAttemptUsage/evaluateAttemptBudget -- a runaway attempt can no longer - // burn through the entire cross-cycle budget before anything reacts. No maxTokens: every driver now - // reports a real per-iteration token count (#5653), but no policy field sets a token ceiling yet -- that - // axis genuinely has no real ceiling to set today, never fabricated. - budget: { - maxTurns: input.amsPolicySpec.capLimits.turns, - maxWallClockMs: input.amsPolicySpec.capLimits.elapsedMs, - maxCostUsd: input.amsPolicySpec.capLimits.budget, - }, - repoFullName: input.repoFullName, - contributorLogin: input.minerLogin, - title: input.codingTaskSpec.title, - body: input.codingTaskSpec.body, - labels: input.codingTaskSpec.labels, - linkedIssues: input.codingTaskSpec.linkedIssues, - branchRef: input.branchRef, - reviewContext: input.reviewContext, - rejectionSignaled: input.rejectionSignaled, - // #6560: the operator's configured self-loop autonomy level reaches the policy the same way every other - // AmsPolicySpec knob above does. It gates only the pass->handoff transition inside iterate-policy.js. - autonomyLevel: input.amsPolicySpec.selfLoopAutonomy, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXR0ZW1wdC1pbnB1dC1idWlsZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiYXR0ZW1wdC1pbnB1dC1idWlsZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBWXZGLDhHQUE4RztBQUM5RyxxR0FBcUc7QUFDckcsOEdBQThHO0FBQzlHLDBEQUEwRDtBQUMxRCxFQUFFO0FBQ0YseUZBQXlGO0FBQ3pGLDhHQUE4RztBQUM5RywwR0FBMEc7QUFDMUcsRUFBRTtBQUNGLGlIQUFpSDtBQUNqSCxpSEFBaUg7QUFDakgsaUhBQWlIO0FBQ2pILDRHQUE0RztBQUM1RyxvR0FBb0c7QUFFcEc7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQWtCRztBQUNILE1BQU0sVUFBVSwyQkFBMkIsQ0FDekMsR0FBdUMsRUFDdkMsYUFBNEIsRUFDNUIsVUFBb0IsRUFDcEIsZ0JBQTRDLEVBQzVDLGlCQUFzQztJQUV0QyxPQUFPO1FBQ0wsZ0JBQWdCLEVBQUUsdUJBQXVCLENBQUMsR0FBRyxDQUFDO1FBQzlDLG9CQUFvQixFQUFFLFVBQVU7UUFDaEMsbUJBQW1CLEVBQUUsMEJBQTBCLENBQUMsR0FBRyxDQUFDO1FBQ3BELFNBQVMsRUFBRSxhQUFhLENBQUMsU0FBUztRQUNsQyxnQkFBZ0IsRUFBRSxnQkFBZ0IsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLEVBQUUsbUJBQW1CLEVBQUUsQ0FBQyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRTtRQUNoSCxHQUFHLENBQUMsaUJBQWlCLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsaUJBQWlCLEVBQUUsQ0FBQztLQUNsRSxDQUFDO0FBQ0osQ0FBQztBQWVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSxxQkFBcUIsQ0FBQyxLQUFpQztJQUNyRSxPQUFPO1FBQ0wsU0FBUyxFQUFFLEtBQUssQ0FBQyxTQUFTO1FBQzFCLGdCQUFnQixFQUFFLEtBQUssQ0FBQyxZQUFZO1FBQ3BDLHNCQUFzQixFQUFFLEtBQUssQ0FBQyxjQUFjLENBQUMsc0JBQXNCO1FBQ25FLFlBQVksRUFBRSxLQUFLLENBQUMsY0FBYyxDQUFDLFlBQVk7UUFDL0MsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJO1FBQ2hCLGFBQWEsRUFBRSxLQUFLLENBQUMsYUFBYSxDQUFDLGFBQWE7UUFDaEQsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLGFBQWEsQ0FBQyxvQkFBb0I7UUFDOUQsdUdBQXVHO1FBQ3ZHLHlHQUF5RztRQUN6Ryx3R0FBd0c7UUFDeEcsb0dBQW9HO1FBQ3BHLHlHQUF5RztRQUN6RyxxRUFBcUU7UUFDckUsTUFBTSxFQUFFO1lBQ04sUUFBUSxFQUFFLEtBQUssQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLEtBQUs7WUFDN0MsY0FBYyxFQUFFLEtBQUssQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLFNBQVM7WUFDdkQsVUFBVSxFQUFFLEtBQUssQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLE1BQU07U0FDakQ7UUFDRCxZQUFZLEVBQUUsS0FBSyxDQUFDLFlBQVk7UUFDaEMsZ0JBQWdCLEVBQUUsS0FBSyxDQUFDLFVBQVU7UUFDbEMsS0FBSyxFQUFFLEtBQUssQ0FBQyxjQUFjLENBQUMsS0FBSztRQUNqQyxJQUFJLEVBQUUsS0FBSyxDQUFDLGNBQWMsQ0FBQyxJQUFJO1FBQy9CLE1BQU0sRUFBRSxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU07UUFDbkMsWUFBWSxFQUFFLEtBQUssQ0FBQyxjQUFjLENBQUMsWUFBWTtRQUMvQyxTQUFTLEVBQUUsS0FBSyxDQUFDLFNBQVM7UUFDMUIsYUFBYSxFQUFFLEtBQUssQ0FBQyxhQUFhO1FBQ2xDLGlCQUFpQixFQUFFLEtBQUssQ0FBQyxpQkFBaUI7UUFDMUMsd0dBQXdHO1FBQ3hHLHNHQUFzRztRQUN0RyxhQUFhLEVBQUUsS0FBSyxDQUFDLGFBQWEsQ0FBQyxnQkFBZ0I7S0FDcEQsQ0FBQztBQUNKLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/attempt-log.d.ts b/packages/loopover-miner/lib/attempt-log.d.ts deleted file mode 100644 index 1d5354d12e..0000000000 --- a/packages/loopover-miner/lib/attempt-log.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AttemptLogEvent } from "@loopover/engine"; -export type AttemptLogEntry = { - id: number; - seq: number; - eventType: string; - attemptId: string; - actionClass: string; - mode: string; - reason: string; - payload: Record; - /** Coding-agent provider name, when the event set one (#5185). Null for every event type that predates this - * field. */ - provider: string | null; - /** Real dollar cost, when the event set one (#5185). Null (not 0) when absent -- never fabricated. */ - costUsd: number | null; - /** Real token count, when some future driver reports one (#5185). Always null today -- no driver reports real - * token usage yet (#5395). */ - tokensUsed: number | null; - createdAt: string; -}; -export type ReadAttemptLogEventsFilter = { - attemptId?: string | null; -}; -export type AttemptLog = { - dbPath: string; - appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry; - readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[]; - exportAttemptLogJsonl(attemptId: string): string; - close(): void; -}; -export declare function resolveAttemptLogDbPath(env?: Record): string; -/** - * Opens the append-only attempt log, creating the table on first use. `seq` is a monotonically increasing counter - * maintained by this module (next = current MAX(seq) + 1) with a UNIQUE(seq) constraint. Rows read back in seq ASC - * order. (#4294) - */ -export declare function initAttemptLog(dbPath?: string): AttemptLog; -export declare function appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry; -export declare function readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[]; -export declare function exportAttemptLogJsonl(attemptId: string): string; -export declare function closeDefaultAttemptLog(): void; diff --git a/packages/loopover-miner/lib/attempt-log.js b/packages/loopover-miner/lib/attempt-log.js deleted file mode 100644 index f8c05fd08f..0000000000 --- a/packages/loopover-miner/lib/attempt-log.js +++ /dev/null @@ -1,183 +0,0 @@ -import { formatAttemptLogJsonl, normalizeAttemptLogEvent } from "@loopover/engine"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -// Append-only driver attempt log (#4294): a structured, attempt-scoped event trace for every CodingAgentDriver run -// (started, tool/edit, succeeded/failed/aborted). IMMUTABILITY INVARIANT: INSERT + SELECT only — rows are never -// rewritten or removed after append. -// -// Why a sibling store instead of extending event-ledger.js: event-ledger is the general miner-loop audit trail -// (discovered_issue, plan_built, pr_prepared, …) keyed by repo scope with a growing free-form type vocabulary. -// Attempt events are keyed by attempt_id, validated against the engine's fixed ATTEMPT_LOG_EVENT_TYPES, and are -// exported per attempt as JSONL — mixing both into one table would couple unrelated lifecycles and complicate the -// per-attempt dump path. This module mirrors governor-ledger.js: engine holds pure normalization, miner holds SQLite. -const defaultDbFileName = "attempt-log.sqlite3"; -let defaultAttemptLog = null; -export function resolveAttemptLogDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_ATTEMPT_LOG_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveAttemptLogDbPath(), "invalid_attempt_log_db_path"); -} -/** Read-filter attempt scope: omitted/nullish → unscoped (all events); otherwise a non-empty attempt id. */ -function normalizeReadAttemptIdFilter(attemptId) { - if (attemptId === undefined || attemptId === null) - return undefined; - if (typeof attemptId !== "string") - throw new Error("invalid_attempt_id"); - const trimmed = attemptId.trim(); - if (!trimmed) - throw new Error("invalid_attempt_id"); - return trimmed; -} -/** Export requires an explicit attempt id — JSONL dumps are always per attempt. */ -function normalizeRequiredAttemptId(attemptId) { - const normalized = normalizeReadAttemptIdFilter(attemptId); - if (normalized === undefined) - throw new Error("invalid_attempt_id"); - return normalized; -} -function rowToEntry(row) { - let payload; - try { - const parsed = JSON.parse(row.payload_json); - if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("corrupted_attempt_log_row"); - } - payload = parsed; - } - catch { - throw new Error("corrupted_attempt_log_row"); - } - return { - id: row.id, - seq: row.seq, - eventType: row.event_type, - attemptId: row.attempt_id, - actionClass: row.action_class, - mode: row.mode, - reason: row.reason, - payload, - provider: row.provider, - costUsd: row.cost_usd, - tokensUsed: row.tokens_used, - createdAt: row.created_at, - }; -} -function rowToNormalized(row) { - return { - eventType: row.event_type, - attemptId: row.attempt_id, - actionClass: row.action_class, - mode: row.mode, - reason: row.reason, - payloadJson: row.payload_json, - provider: row.provider, - costUsd: row.cost_usd, - tokensUsed: row.tokens_used, - }; -} -// Add the provider/cost_usd/tokens_used columns (#5185) to an on-disk file created before they existed. `CREATE -// TABLE IF NOT EXISTS` above is a no-op against an already-existing table, so a pre-#5185 file needs this -// explicit ALTER -- guarded by a per-column presence check (same technique as governor-state.js's own -// ensurePauseColumns) so a file missing only one of the three still gets exactly what it's missing. -function ensureOutcomeColumns(db) { - const existingColumns = new Set(db.prepare("PRAGMA table_info(attempt_log_events)").all().map((column) => column.name)); - if (!existingColumns.has("provider")) { - db.exec("ALTER TABLE attempt_log_events ADD COLUMN provider TEXT"); - } - if (!existingColumns.has("cost_usd")) { - db.exec("ALTER TABLE attempt_log_events ADD COLUMN cost_usd REAL"); - } - if (!existingColumns.has("tokens_used")) { - db.exec("ALTER TABLE attempt_log_events ADD COLUMN tokens_used INTEGER"); - } -} -/** - * Opens the append-only attempt log, creating the table on first use. `seq` is a monotonically increasing counter - * maintained by this module (next = current MAX(seq) + 1) with a UNIQUE(seq) constraint. Rows read back in seq ASC - * order. (#4294) - */ -export function initAttemptLog(dbPath = resolveAttemptLogDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS attempt_log_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - seq INTEGER NOT NULL UNIQUE, - attempt_id TEXT NOT NULL, - event_type TEXT NOT NULL, - action_class TEXT NOT NULL, - mode TEXT NOT NULL, - reason TEXT NOT NULL, - payload_json TEXT NOT NULL, - created_at TEXT NOT NULL - ) - `); - ensureOutcomeColumns(db); - db.exec("CREATE INDEX IF NOT EXISTS idx_attempt_log_attempt ON attempt_log_events (attempt_id, seq)"); - const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM attempt_log_events"); - const appendStatement = db.prepare(` - INSERT INTO attempt_log_events ( - seq, attempt_id, event_type, action_class, mode, reason, payload_json, provider, cost_usd, tokens_used, - created_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - const getByIdStatement = db.prepare("SELECT * FROM attempt_log_events WHERE id = ?"); - const readAllStatement = db.prepare("SELECT * FROM attempt_log_events ORDER BY seq ASC"); - const readByAttemptStatement = db.prepare("SELECT * FROM attempt_log_events WHERE attempt_id = ? ORDER BY seq ASC"); - return { - dbPath: resolvedPath, - appendAttemptLogEvent(event) { - const normalized = normalizeAttemptLogEvent(event); - const createdAt = new Date().toISOString(); - db.exec("BEGIN IMMEDIATE"); - try { - const nextSeqRow = nextSeqStatement.get(); - const nextSeq = nextSeqRow.nextSeq; - const result = appendStatement.run(nextSeq, normalized.attemptId, normalized.eventType, normalized.actionClass, normalized.mode, normalized.reason, normalized.payloadJson, normalized.provider, normalized.costUsd, normalized.tokensUsed, createdAt); - const entry = rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid))); - db.exec("COMMIT"); - return entry; - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - }, - readAttemptLogEvents(filter = {}) { - const attemptId = normalizeReadAttemptIdFilter(filter.attemptId); - const rows = attemptId === undefined - ? readAllStatement.all() - : readByAttemptStatement.all(attemptId); - return rows.map(rowToEntry); - }, - exportAttemptLogJsonl(attemptId) { - const scopedAttemptId = normalizeRequiredAttemptId(attemptId); - const rows = readByAttemptStatement.all(scopedAttemptId); - return formatAttemptLogJsonl(rows.map(rowToNormalized)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultAttemptLog() { - defaultAttemptLog ??= initAttemptLog(); - return defaultAttemptLog; -} -export function appendAttemptLogEvent(event) { - return getDefaultAttemptLog().appendAttemptLogEvent(event); -} -export function readAttemptLogEvents(filter) { - return getDefaultAttemptLog().readAttemptLogEvents(filter); -} -export function exportAttemptLogJsonl(attemptId) { - return getDefaultAttemptLog().exportAttemptLogJsonl(attemptId); -} -export function closeDefaultAttemptLog() { - if (!defaultAttemptLog) - return; - defaultAttemptLog.close(); - defaultAttemptLog = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXR0ZW1wdC1sb2cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdHRlbXB0LWxvZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUscUJBQXFCLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUduRixPQUFPLEVBQUUseUJBQXlCLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUV4RyxtSEFBbUg7QUFDbkgsZ0hBQWdIO0FBQ2hILHFDQUFxQztBQUNyQyxFQUFFO0FBQ0YsK0dBQStHO0FBQy9HLCtHQUErRztBQUMvRyxnSEFBZ0g7QUFDaEgsa0hBQWtIO0FBQ2xILHNIQUFzSDtBQUV0SCxNQUFNLGlCQUFpQixHQUFHLHFCQUFxQixDQUFDO0FBQ2hELElBQUksaUJBQWlCLEdBQXNCLElBQUksQ0FBQztBQW1EaEQsTUFBTSxVQUFVLHVCQUF1QixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQzNGLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsK0JBQStCLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDMUYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsdUJBQXVCLEVBQUUsRUFBRSw2QkFBNkIsQ0FBQyxDQUFDO0FBQ3JHLENBQUM7QUFFRCw0R0FBNEc7QUFDNUcsU0FBUyw0QkFBNEIsQ0FBQyxTQUFrQjtJQUN0RCxJQUFJLFNBQVMsS0FBSyxTQUFTLElBQUksU0FBUyxLQUFLLElBQUk7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUNwRSxJQUFJLE9BQU8sU0FBUyxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLG9CQUFvQixDQUFDLENBQUM7SUFDekUsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ2pDLElBQUksQ0FBQyxPQUFPO1FBQUUsTUFBTSxJQUFJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/attempt-runner.d.ts b/packages/loopover-miner/lib/attempt-runner.d.ts deleted file mode 100644 index b440eb31e9..0000000000 --- a/packages/loopover-miner/lib/attempt-runner.d.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { CodingAgentDriver, GovernorDecision, IterateLoopInput, IterateLoopResult, IterateLoopShouldAbort, LocalWriteActionSpec } from "@loopover/engine"; -import type { FreshnessAbortReason, LiveIssueSnapshot, SubmissionFreshnessClaimLedger } from "./submission-freshness-check.js"; -import type { GovernorChokepointInputPersisted } from "./governor-chokepoint-persisted.js"; -import type { GovernorState } from "./governor-state.js"; -import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js"; -export declare const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"]; -export type AttemptGovernorContext = Omit; -export type AttemptInput = { - loopInput: IterateLoopInput; - issueNumber: number; - minerLogin: string; - base: string; - killSwitchScope: "global" | "repo" | "none"; - slopThreshold: "clean" | "low" | "elevated" | "high"; - submissionMode: "observe" | "enforce"; - maxConsecutiveGateBlocks?: number; - draft?: boolean; - governor: AttemptGovernorContext; -}; -export type AttemptDeps = { - driver: CodingAgentDriver; - runSlopAssessment: (input: unknown) => unknown; - appendAttemptLogEvent: (event: unknown) => void; - claimLedger: SubmissionFreshnessClaimLedger; - fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise; - eventLedger: HarnessSubmissionEventLedger; - /** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted - * falls back to that function's own default (the real default governor ledger). */ - governorLedgerAppend?: (event: unknown) => unknown; - /** Injected governor-state store (#5134); omitted falls back to evaluateGovernorChokepointGatePersisted's - * own default (opens + closes the real default governor-state store for this one call). */ - governorState?: GovernorState; - sessionStartMs?: number; - nowMs: number; - executeLocalWrite: (spec: LocalWriteActionSpec) => Promise; - /** Mid-attempt kill-switch probe threaded into `runIterateLoop` (#5670). */ - shouldAbort?: () => IterateLoopShouldAbort; - /** Live kill-switch scope resolver after handoff (#5670); defaults to the frozen attempt-start scope. */ - resolveKillSwitchScope?: () => "global" | "repo" | "none"; -}; -export type AttemptResult = { - outcome: "abandon"; - loopResult: IterateLoopResult; -} | { - outcome: "stale"; - reason: FreshnessAbortReason; - loopResult: IterateLoopResult; -} | { - outcome: "blocked"; - decision: HarnessSubmissionDecision; - loopResult: IterateLoopResult; -} | { - outcome: "governed"; - decision: GovernorDecision; - loopResult: IterateLoopResult; -} | { - outcome: "submitted"; - spec: LocalWriteActionSpec; - execResult: unknown; - loopResult: IterateLoopResult; -}; -/** - * Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor - * chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed - * input/deps, mirroring every sibling module in this pipeline. - */ -export declare function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise; diff --git a/packages/loopover-miner/lib/attempt-runner.js b/packages/loopover-miner/lib/attempt-runner.js deleted file mode 100644 index 90764dae16..0000000000 --- a/packages/loopover-miner/lib/attempt-runner.js +++ /dev/null @@ -1,220 +0,0 @@ -import { buildOpenPrSpec, fingerprintFromChangedFiles } from "@loopover/engine"; -import { runIterateLoop } from "@loopover/engine"; -import { checkSubmissionFreshness } from "./submission-freshness-check.js"; -import { evaluateGovernorChokepointGatePersisted } from "./governor-chokepoint-persisted.js"; -import { listRecentOwnSubmissions } from "./governor-state.js"; -import { prepareOpenPrSubmission } from "./harness-submission-trigger.js"; -import { captureMinerError } from "./sentry.js"; -export const ATTEMPT_OUTCOMES = Object.freeze([ - "abandon", - "stale", - "blocked", - "governed", - "submitted", -]); -// The real driving-loop entrypoint (#2337): the missing link between #2333's iterate-loop orchestrator and an -// actual, executed open_pr write. Composes, in order: runIterateLoop (create -> score -> self-review -> decide, -// #2333) -> on handoff, checkSubmissionFreshness (#3007) -> prepareOpenPrSubmission (#2336/#2337) -> the -// Governor chokepoint (#2340, which itself composes kill-switch, dry-run, rate-limit, budget caps, non- -// convergence, self-reputation-throttle, and self-plagiarism -- see chokepoint.ts's own module doc comment for -// the exact precedence ladder) -> on allowed:true, builds the REAL open_pr command via the now-shared -// buildOpenPrSpec (@loopover/engine, moved from root src/mcp/local-write-tools.ts) and executes it. -// -// WORKTREE LIFECYCLE IS NOT THIS MODULE'S JOB: runIterateLoop already takes a plain `workingDirectory` string -// (packages/loopover-engine/src/miner/iterate-loop.ts's own IterateLoopInput), deliberately agnostic about -// where it came from. Allocating one is the caller's job, via the already-built slot allocator -// (worktree-allocator.js, #4297) -- this module composes the create/review/gate/submit sequence #2337 is -// actually about, not worktree allocation policy, which is a separate, already-solved concern. -// -// `deps.runSlopAssessment` stays INJECTED rather than imported here (this module composes the sequence and is -// agnostic about the scorer behind the seam), but it is no longer unwired: slop-assessment.js (#5133) is its -// real production binding -- a direct pass-through to the engine's own buildSlopAssessment -- and attempt-cli.js -// wires that binding in on the production path. The seam was injected-but-unwired when this module was written -// only because the deterministic scorer was not yet portable; #5133 extracted src/signals/slop.ts's PR-side -// scorer into packages/loopover-engine/src/signals/slop.ts (byte-parity-verified against the live gate's own -// copy), closing the gap this header used to document. This function still requires a real implementation be -// injected rather than silently stubbing a result that would either always pass (unsafe) or always fail -// (useless). -// -// `input.governor`'s cross-attempt state (rate-limit buckets, backoff attempts, budget-cap usage) DOES now -// persist across separate process invocations (#5134, governor-state.js), via -// evaluateGovernorChokepointGatePersisted -- callers no longer need to hand-thread honest empty/zero defaults -// on every invocation; `capUsage` is loaded from that same store but its post-attempt save stays the caller's -// job (see governor-chokepoint-persisted.js's own header for why: nothing computes "the next capUsage" from a -// verdict, only the attempt's real outcome does). Self-plagiarism state is now wired here (#5676): the -// prospective submission's real diff `selfPlagiarismCandidate` (fingerprintFromChangedFiles over the handoff -// packet's changed files) and the miner's real `selfPlagiarismRecentSubmissions` (governor-state.js's -// listRecentOwnSubmissions) are computed late -- right before the chokepoint call, after handoff, where the -// changed files first exist -- and passed in, so chokepoint.ts's selfPlagiarismCheck finally runs on real data. -// `input.governor.reputationHistory` remains a caller-supplied optional field, not auto-loaded here yet. -function assertFn(value, name) { - if (typeof value !== "function") - throw new Error(`invalid_${name}`); -} -function assertDeps(deps) { - if (!deps || typeof deps !== "object") - throw new Error("invalid_attempt_deps"); - const candidate = deps; - assertFn(candidate.runSlopAssessment, "run_slop_assessment"); - assertFn(candidate.appendAttemptLogEvent, "append_attempt_log_event"); - assertFn(candidate.fetchLiveIssueSnapshot, "fetch_live_issue_snapshot"); - assertFn(candidate.executeLocalWrite, "execute_local_write"); - const driver = candidate.driver; - if (!driver || typeof driver.run !== "function") - throw new Error("invalid_driver"); - const claimLedger = candidate.claimLedger; - if (!claimLedger || typeof claimLedger.listClaims !== "function") - throw new Error("invalid_claim_ledger"); - const eventLedger = candidate.eventLedger; - if (!eventLedger || typeof eventLedger.appendEvent !== "function") - throw new Error("invalid_event_ledger"); - if (typeof candidate.nowMs !== "number" || !Number.isFinite(candidate.nowMs)) - throw new Error("invalid_now_ms"); -} -function assertInput(input) { - if (!input || typeof input !== "object") - throw new Error("invalid_attempt_input"); - const candidate = input; - if (!candidate.loopInput || typeof candidate.loopInput !== "object") - throw new Error("invalid_loop_input"); - if (!Number.isInteger(candidate.issueNumber) || candidate.issueNumber < 1) { - throw new Error("invalid_issue_number"); - } - if (typeof candidate.minerLogin !== "string" || !candidate.minerLogin.trim()) - throw new Error("invalid_miner_login"); - if (typeof candidate.base !== "string" || !candidate.base.trim()) - throw new Error("invalid_base"); - if (!["global", "repo", "none"].includes(candidate.killSwitchScope)) { - throw new Error("invalid_kill_switch_scope"); - } - if (!["clean", "low", "elevated", "high"].includes(candidate.slopThreshold)) { - throw new Error("invalid_slop_threshold"); - } - if (!["observe", "enforce"].includes(candidate.submissionMode)) - throw new Error("invalid_submission_mode"); - if (!candidate.governor || typeof candidate.governor !== "object") - throw new Error("invalid_governor_context"); -} -/** - * Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor - * chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed - * input/deps, mirroring every sibling module in this pipeline. - */ -export async function runMinerAttempt(input, deps) { - assertInput(input); - assertDeps(deps); - const loopResult = await runIterateLoop(input.loopInput, { - driver: deps.driver, - // AttemptDeps deliberately keeps this loosely typed (`(input: unknown) => unknown`) at the public boundary -- - // the real, narrower shape (SelfReviewAdapterDeps["runSlopAssessment"]) is an engine-internal detail callers - // shouldn't need to import just to satisfy this dependency's type. - runSlopAssessment: deps.runSlopAssessment, - appendAttemptLogEvent: deps.appendAttemptLogEvent, - ...(typeof deps.shouldAbort === "function" ? { shouldAbort: deps.shouldAbort } : {}), - }); - if (loopResult.outcome === "abandon") { - return { outcome: "abandon", loopResult }; - } - // Populated by design whenever outcome !== "abandon" (IterateLoopOutcome is only ever "handoff" | "abandon", - // and handoffPacket is only ever set on "handoff") -- runIterateLoop's own type keeps the field optional - // because it models both outcomes in one result shape. - const handoffPacket = loopResult.handoffPacket; - // Re-check kill-switch AFTER handoff and BEFORE any write (#5670) when a live resolver is supplied. - // Without a live resolver, preserve pre-#5670 behavior: the frozen attempt-start scope is threaded into - // prepareOpenPrSubmission / the submission gate (which itself denies active kill scopes). - if (typeof deps.resolveKillSwitchScope === "function") { - const liveKillSwitchScope = deps.resolveKillSwitchScope(); - if (liveKillSwitchScope !== "none") { - return { - outcome: "abandon", - loopResult: { - ...loopResult, - outcome: "abandon", - finalDecision: { - action: "abandon", - abandonReason: "kill_switch_engaged", - reason: `Kill-switch (${liveKillSwitchScope}) engaged after handoff; refusing to open a PR.`, - }, - handoffPacket: undefined, - }, - }; - } - } - const freshness = await checkSubmissionFreshness({ repoFullName: input.loopInput.repoFullName, issueNumber: input.issueNumber, minerLogin: input.minerLogin }, { claimLedger: deps.claimLedger, fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, eventLedger: deps.eventLedger }); - if (!freshness.fresh) { - return { outcome: "stale", reason: freshness.reason, loopResult }; - } - const submission = await prepareOpenPrSubmission({ - killSwitchScope: input.killSwitchScope, - repoFullName: input.loopInput.repoFullName, - // HandoffPacket's optional fields are typed `| undefined` (engine's exactOptionalPropertyTypes style); - // HarnessSubmissionCandidateInput's narrower local field type omits that -- both describe the same real - // shape, so the cast just bridges the two independently-declared-but-compatible types. - handoffPacket: handoffPacket, - slopThreshold: input.slopThreshold, - mode: input.submissionMode, - // Spread-omit rather than pass `undefined` explicitly -- PrepareOpenPrSubmissionCandidate's optional - // fields don't declare `| undefined`, and exactOptionalPropertyTypes treats those as different. - ...(input.maxConsecutiveGateBlocks !== undefined - ? { maxConsecutiveGateBlocks: input.maxConsecutiveGateBlocks } - : {}), - base: input.base, - title: input.loopInput.title, - body: input.loopInput.body ?? "", - ...(input.draft !== undefined ? { draft: input.draft } : {}), - }, { eventLedger: deps.eventLedger, ...(deps.sessionStartMs !== undefined ? { sessionStartMs: deps.sessionStartMs } : {}) }); - if (!submission.ready) { - return { outcome: "blocked", decision: submission.decision, loopResult }; - } - // Late-augment the self-plagiarism inputs (#5676): the prospective submission's real diff fingerprint and the - // miner's real recent-submission history only exist HERE, after handoff -- attempt-cli.js's single early - // governor snapshot (buildAttemptGovernorContext) is built before any changed files exist, so it cannot carry - // them. This finally feeds chokepoint.ts's selfPlagiarismCheck, which was previously always skipped for lack of - // data. Read the history from the SAME governor-state store the chokepoint itself uses (deps.governorState when - // provided, else the persisted default via the module-level export). Fail open on a read failure so a - // history-store hiccup never blocks an otherwise-allowed real submission. - /* v8 ignore next -- buildHandoffPacket always populates changedFiles; the `?? []` only guards a hand-built packet */ - const changedFilePaths = (handoffPacket.changedFiles ?? []).map((file) => file.path); - const selfPlagiarismCandidate = { - repoFullName: input.loopInput.repoFullName, - fingerprint: fingerprintFromChangedFiles(changedFilePaths), - // The prospective submission's own time is "now" -- selfPlagiarismCheck needs a real submittedAt on the - // candidate (it denies a candidate lacking one) and uses it for earliest-claimant election vs the priors. - submittedAt: new Date(deps.nowMs).toISOString(), - }; - let selfPlagiarismRecentSubmissions; - try { - selfPlagiarismRecentSubmissions = deps.governorState - ? deps.governorState.listRecentOwnSubmissions({ repoFullName: input.loopInput.repoFullName }) - : listRecentOwnSubmissions({ repoFullName: input.loopInput.repoFullName }); - } - catch (error) { - // Fail-open is deliberate (see the comment above) -- this only makes the fallback VISIBLE. A broken - // governor-state store silently disabling the self-plagiarism safety check had zero trace anywhere (#6011). - captureMinerError(error, { kind: "self_plagiarism_history_read_failed", repoFullName: input.loopInput.repoFullName }); - selfPlagiarismRecentSubmissions = []; - } - const governed = evaluateGovernorChokepointGatePersisted({ - actionClass: "open_pr", - repoFullName: input.loopInput.repoFullName, - nowMs: deps.nowMs, - wouldBeAction: submission.openPrInput, - ...input.governor, - selfPlagiarismCandidate, - selfPlagiarismRecentSubmissions, - }, { - // AttemptDeps deliberately keeps governorLedgerAppend loosely typed (`(event: unknown) => unknown`) at the - // public boundary, same as runSlopAssessment above -- cast to the engine-internal signature here. - ...(deps.governorLedgerAppend - ? { append: deps.governorLedgerAppend } - : {}), - ...(deps.governorState ? { governorState: deps.governorState } : {}), - }); - if (!governed.decision.allowed) { - return { outcome: "governed", decision: governed.decision, loopResult }; - } - const spec = buildOpenPrSpec(submission.openPrInput); - const execResult = await deps.executeLocalWrite(spec); - return { outcome: "submitted", spec, execResult, loopResult }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXR0ZW1wdC1ydW5uZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdHRlbXB0LXJ1bm5lci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFVQSxPQUFPLEVBQUUsZUFBZSxFQUFFLDJCQUEyQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDaEYsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBTWxELE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRTNFLE9BQU8sRUFBRSx1Q0FBdUMsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBRzdGLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBTS9ELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVoRCxNQUFNLENBQUMsTUFBTSxnQkFBZ0IsR0FBc0UsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUMvRyxTQUFTO0lBQ1QsT0FBTztJQUNQLFNBQVM7SUFDVCxVQUFVO0lBQ1YsV0FBVztDQUNaLENBQUMsQ0FBQztBQXFESCw4R0FBOEc7QUFDOUcsZ0hBQWdIO0FBQ2hILHlHQUF5RztBQUN6Ryx3R0FBd0c7QUFDeEcsK0dBQStHO0FBQy9HLHNHQUFzRztBQUN0RyxvR0FBb0c7QUFDcEcsRUFBRTtBQUNGLDhHQUE4RztBQUM5RywyR0FBMkc7QUFDM0csK0ZBQStGO0FBQy9GLHlHQUF5RztBQUN6RywrRkFBK0Y7QUFDL0YsRUFBRTtBQUNGLDhHQUE4RztBQUM5Ryw2R0FBNkc7QUFDN0csaUhBQWlIO0FBQ2pILCtHQUErRztBQUMvRyw0R0FBNEc7QUFDNUcsNkdBQTZHO0FBQzdHLDZHQUE2RztBQUM3Ryx3R0FBd0c7QUFDeEcsYUFBYTtBQUNiLEVBQUU7QUFDRiwyR0FBMkc7QUFDM0csOEVBQThFO0FBQzlFLDhHQUE4RztBQUM5Ryw4R0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/attempt-worktree.d.ts b/packages/loopover-miner/lib/attempt-worktree.d.ts deleted file mode 100644 index 8770375570..0000000000 --- a/packages/loopover-miner/lib/attempt-worktree.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { WorktreeExecFn } from "@loopover/engine"; -import type { RunGitFn } from "./repo-clone.js"; -/** - * Real child_process-backed implementation of the engine's WorktreeExecFn contract. Resolves (never - * rejects) on error/timeout, mirroring coding-agent-construction.js's createRealCliSubprocessSpawn -- a - * failed `git worktree add`'s stderr is the diagnosable signal, not something to lose to an unhandled - * rejection. - */ -export declare function createRealWorktreeExec(timeoutMs?: number): WorktreeExecFn; -export type PrepareAttemptWorktreeOptions = { - baseBranch?: string; - cloneBaseDir?: string; - env?: Record; - exec?: WorktreeExecFn; - timeoutMs?: number; - remoteUrl?: string; - runGit?: RunGitFn; -}; -export type PrepareAttemptWorktreeResult = { - ok: true; - worktreePath: string; - branchName: string; - repoPath: string; -} | { - ok: false; - repoPath?: string; - error: string; -}; -/** - * Prepare a real, isolated git worktree for one attempt: ensure the target repo's base clone exists and is - * current, then create a fresh `git worktree` off it on a deterministically-named branch. Fails closed - * (`ok: false`) on any step's failure rather than handing back a half-prepared directory. - */ -export declare function prepareAttemptWorktree(repoFullName: string, attemptId: string, options?: PrepareAttemptWorktreeOptions): Promise; -/** - * Tear down an attempt's worktree once the attempt concludes, per the engine's own retention policy: a - * failed attempt's worktree is RETAINED for post-mortem inspection, a succeeded one is removed. - */ -export declare function cleanupAttemptWorktree(repoPath: string, worktreePath: string, attemptOk: boolean, options?: { - exec?: WorktreeExecFn; - timeoutMs?: number; -}): Promise<{ - ok: boolean; - removed: boolean; - error?: string; -}>; diff --git a/packages/loopover-miner/lib/attempt-worktree.js b/packages/loopover-miner/lib/attempt-worktree.js deleted file mode 100644 index 2954348975..0000000000 --- a/packages/loopover-miner/lib/attempt-worktree.js +++ /dev/null @@ -1,79 +0,0 @@ -import { spawn } from "node:child_process"; -import { addWorktree, removeWorktree, shouldRetainWorktree } from "@loopover/engine"; -import { ensureRepoCloned } from "./repo-clone.js"; -// Real attempt-worktree preparation (#5132, Wave 3.5 follow-up). Composes ensureRepoCloned (repo-clone.js, -// the missing base-clone-management step) with @loopover/engine's already-built, already-tested -// addWorktree/removeWorktree primitives -- which existed but were never called from this package, so -// `workingDirectory` handed to runIterateLoop was always just an empty directory with no real git repo in -// it. This is the caller that finally exercises them for real. -const DEFAULT_TIMEOUT_MS = 120_000; -/** - * Real child_process-backed implementation of the engine's WorktreeExecFn contract. Resolves (never - * rejects) on error/timeout, mirroring coding-agent-construction.js's createRealCliSubprocessSpawn -- a - * failed `git worktree add`'s stderr is the diagnosable signal, not something to lose to an unhandled - * rejection. - */ -export function createRealWorktreeExec(timeoutMs = DEFAULT_TIMEOUT_MS) { - return (cmd, args, opts) => new Promise((resolve) => { - const child = spawn(cmd, [...args], { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] }); - let stdout = ""; - let stderr = ""; - const timer = setTimeout(() => { - child.kill("SIGKILL"); - resolve({ code: null, stdout, stderr: `${stderr}\ntimed_out_after_${timeoutMs}ms`.trim() }); - }, timeoutMs); - child.stdout?.on("data", (chunk) => { - stdout += chunk.toString("utf8"); - }); - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString("utf8"); - }); - child.on("error", (err) => { - clearTimeout(timer); - resolve({ code: null, stdout, stderr: err.message }); - }); - child.on("close", (code) => { - clearTimeout(timer); - resolve({ code, stdout, stderr }); - }); - }); -} -/** - * Prepare a real, isolated git worktree for one attempt: ensure the target repo's base clone exists and is - * current, then create a fresh `git worktree` off it on a deterministically-named branch. Fails closed - * (`ok: false`) on any step's failure rather than handing back a half-prepared directory. - */ -export async function prepareAttemptWorktree(repoFullName, attemptId, options = {}) { - // Spread-omit rather than pass `undefined` explicitly -- EnsureRepoClonedOptions' optional fields don't - // declare `| undefined`, and exactOptionalPropertyTypes treats those as different. - const cloneResult = await ensureRepoCloned(repoFullName, { - ...(options.baseBranch !== undefined ? { baseBranch: options.baseBranch } : {}), - ...(options.cloneBaseDir !== undefined ? { cloneBaseDir: options.cloneBaseDir } : {}), - ...(options.env !== undefined ? { env: options.env } : {}), - ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}), - ...(options.remoteUrl !== undefined ? { remoteUrl: options.remoteUrl } : {}), - ...(options.runGit !== undefined ? { runGit: options.runGit } : {}), - }); - // ensureRepoCloned's own EnsureRepoClonedResult declares `error` optional, but every one of its real ok:false - // return sites (repo-clone.ts) sets a real, non-empty error string -- a non-null assertion here rather than a - // fake fallback string, since that fallback would be genuinely unreachable dead code. - if (!cloneResult.ok) - return { ok: false, error: cloneResult.error }; - const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs); - const baseBranch = typeof options.baseBranch === "string" && options.baseBranch.trim() ? options.baseBranch.trim() : "main"; - const added = await addWorktree({ exec, repoPath: cloneResult.repoPath, baseBranch, attemptId }); - // Same reasoning as above: the engine's addWorktree always sets a real error string (git stderr or a synthetic - // exit-code message) on ok:false -- see worktree-plan.ts's addWorktree. - if (!added.ok) - return { ok: false, repoPath: cloneResult.repoPath, error: added.error }; - return { ok: true, worktreePath: added.plan.worktreePath, branchName: added.plan.branchName, repoPath: cloneResult.repoPath }; -} -/** - * Tear down an attempt's worktree once the attempt concludes, per the engine's own retention policy: a - * failed attempt's worktree is RETAINED for post-mortem inspection, a succeeded one is removed. - */ -export async function cleanupAttemptWorktree(repoPath, worktreePath, attemptOk, options = {}) { - const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs); - return removeWorktree({ exec, repoPath, worktreePath, retain: shouldRetainWorktree(attemptOk) }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXR0ZW1wdC13b3JrdHJlZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImF0dGVtcHQtd29ya3RyZWUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzNDLE9BQU8sRUFBRSxXQUFXLEVBQUUsY0FBYyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFckYsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFHbkQsMkdBQTJHO0FBQzNHLGdHQUFnRztBQUNoRyxxR0FBcUc7QUFDckcsMEdBQTBHO0FBQzFHLCtEQUErRDtBQUUvRCxNQUFNLGtCQUFrQixHQUFHLE9BQU8sQ0FBQztBQUVuQzs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxTQUFTLEdBQUcsa0JBQWtCO0lBQ25FLE9BQU8sQ0FBQyxHQUFHLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxFQUFFLENBQ3pCLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUU7UUFDdEIsTUFBTSxLQUFLLEdBQUcsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLEdBQUcsSUFBSSxDQUFDLEVBQUUsRUFBRSxHQUFHLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRSxLQUFLLEVBQUUsQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUMxRixJQUFJLE1BQU0sR0FBRyxFQUFFLENBQUM7UUFDaEIsSUFBSSxNQUFNLEdBQUcsRUFBRSxDQUFDO1FBQ2hCLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxHQUFHLEVBQUU7WUFDNUIsS0FBSyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQztZQUN0QixPQUFPLENBQUMsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsR0FBRyxNQUFNLHFCQUFxQixT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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/calibration-cli.d.ts b/packages/loopover-miner/lib/calibration-cli.d.ts deleted file mode 100644 index 7d68704600..0000000000 --- a/packages/loopover-miner/lib/calibration-cli.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { LedgerEntry } from "./event-ledger.js"; -import type { PredictionLedgerEntry } from "./prediction-ledger.js"; -import type { PredictedVerdictRecord, ObservedOutcomeRecord } from "./calibration-types.js"; -/** Map prediction-ledger rows to predicted-verdict records: the target id becomes a string key and the recorded - * prediction verdict is the `conclusion`. Exported so callers other than this CLI (the MCP calibration-report - * tool, #5821) can build the identical join without re-implementing the mapping. */ -export declare function toPredictionRecords(rows: PredictionLedgerEntry[]): PredictedVerdictRecord[]; -/** Reduce the append-only `pr_outcome` event stream to the LATEST observed outcome per (repo, PR), as - * observed-outcome records. `recordedAt` comes from the event's own timestamp (always present), so an outcome is - * never dropped for lacking a `closedAt`. Malformed payloads are skipped. Exported for the same reason as - * {@link toPredictionRecords} above. */ -export declare function toOutcomeRecords(events: LedgerEntry[]): ObservedOutcomeRecord[]; -/** - * Run `loopover-miner calibration [--json]`. Reads the prediction ledger + PR-outcome events, joins them into a - * calibration report, and prints it (a JSON dump under `--json`, else a per-project text summary). Returns the - * process exit code: 0 on success, 1 on an unknown option. - */ -export declare function runCalibrationCli(args?: string[], env?: Record): number; diff --git a/packages/loopover-miner/lib/calibration-cli.js b/packages/loopover-miner/lib/calibration-cli.js deleted file mode 100644 index 62f167c789..0000000000 --- a/packages/loopover-miner/lib/calibration-cli.js +++ /dev/null @@ -1,93 +0,0 @@ -// `loopover-miner calibration [--json]` (#4849): a read-only report joining the miner's own predicted gate -// verdicts (prediction-ledger) with the realized PR outcomes it later observed (event-ledger `pr_outcome` -// events), via the pure buildCalibrationReport join. Opens both local stores, maps their rows to the -// calibration record shapes, renders, and closes. Never modifies the live scoring/calibration logic. -import { buildCalibrationReport } from "./calibration.js"; -import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js"; -import { MINER_PR_OUTCOME_EVENT } from "./pr-outcome.js"; -import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js"; -import { reportCliFailure, describeCliError } from "./cli-error.js"; -const CALIBRATION_USAGE = "Usage: loopover-miner calibration [--json]"; -/** Map prediction-ledger rows to predicted-verdict records: the target id becomes a string key and the recorded - * prediction verdict is the `conclusion`. Exported so callers other than this CLI (the MCP calibration-report - * tool, #5821) can build the identical join without re-implementing the mapping. */ -export function toPredictionRecords(rows) { - return rows.map((row) => ({ - project: row.repoFullName, - targetId: String(row.targetId), - predictedDecision: row.conclusion, - recordedAt: row.ts, - })); -} -/** Reduce the append-only `pr_outcome` event stream to the LATEST observed outcome per (repo, PR), as - * observed-outcome records. `recordedAt` comes from the event's own timestamp (always present), so an outcome is - * never dropped for lacking a `closedAt`. Malformed payloads are skipped. Exported for the same reason as - * {@link toPredictionRecords} above. */ -export function toOutcomeRecords(events) { - const latest = new Map(); - for (const event of events) { - if (event?.type !== MINER_PR_OUTCOME_EVENT) - continue; - const payload = event.payload; - if (!payload || !Number.isInteger(payload.prNumber) || typeof payload.decision !== "string") - continue; - latest.set(`${event.repoFullName}:${payload.prNumber}`, { - // ObservedOutcomeRecord.project is declared non-nullable, but LedgerEntry.repoFullName is `string | null` - // for other event kinds; a pr_outcome event always carries a real repoFullName in practice, so this passes - // the value through unchanged rather than substituting a fallback that would be a behavior change. - project: event.repoFullName, - targetId: String(payload.prNumber), - outcomeDecision: payload.decision, - recordedAt: event.createdAt, - }); - } - return [...latest.values()]; -} -function renderReportText(report) { - if (!report.hasSignal) { - console.log("calibration: no decided predictions yet (predictions need a realized merge/close outcome)."); - return; - } - for (const row of report.rows) { - const merge = row.mergePrecision === null ? "n/a" : `${Math.round(row.mergePrecision * 100)}%`; - const close = row.closePrecision === null ? "n/a" : `${Math.round(row.closePrecision * 100)}%`; - console.log(`${row.project}: ${row.decided} decided | ` + - `merge ${row.mergeConfirmed}/${row.wouldMerge} (${merge}) | ` + - `close ${row.closeConfirmed}/${row.wouldClose} (${close}) | hold ${row.hold}`); - } -} -/** - * Run `loopover-miner calibration [--json]`. Reads the prediction ledger + PR-outcome events, joins them into a - * calibration report, and prints it (a JSON dump under `--json`, else a per-project text summary). Returns the - * process exit code: 0 on success, 1 on an unknown option. - */ -export function runCalibrationCli(args = [], env = process.env) { - const json = args.includes("--json"); - // This command takes no positional arguments, so anything that is not `--json` is a mistake -- including a - // bare positional (`calibration foo`), which a `startsWith("-")` check silently let through (#5834). Mirrors - // the strict zero-positional discipline `ledger list` (event-ledger-cli.js) already applies. - const unknown = args.find((token) => token !== "--json"); - if (unknown) { - return reportCliFailure(json, `Unknown option: ${unknown}. ${CALIBRATION_USAGE}`, 1); - } - let predictionStore; - let eventLedger; - try { - predictionStore = initPredictionLedger(resolvePredictionLedgerDbPath(env)); - eventLedger = initEventLedger(resolveEventLedgerDbPath(env)); - const report = buildCalibrationReport(toPredictionRecords(predictionStore.readPredictions()), toOutcomeRecords(eventLedger.readEvents())); - if (json) - console.log(JSON.stringify(report, null, 2)); - else - renderReportText(report); - return 0; - } - catch (error) { - return reportCliFailure(json, describeCliError(error)); - } - finally { - predictionStore?.close(); - eventLedger?.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsaWJyYXRpb24tY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2FsaWJyYXRpb24tY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDJHQUEyRztBQUMzRywwR0FBMEc7QUFDMUcscUdBQXFHO0FBQ3JHLHFHQUFxRztBQUNyRyxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUMxRCxPQUFPLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFOUUsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDekQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLDZCQUE2QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFHN0YsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFcEUsTUFBTSxpQkFBaUIsR0FBRyw0Q0FBNEMsQ0FBQztBQUV2RTs7cUZBRXFGO0FBQ3JGLE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxJQUE2QjtJQUMvRCxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDeEIsT0FBTyxFQUFFLEdBQUcsQ0FBQyxZQUFZO1FBQ3pCLFFBQVEsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQztRQUM5QixpQkFBaUIsRUFBRSxHQUFHLENBQUMsVUFBVTtRQUNqQyxVQUFVLEVBQUUsR0FBRyxDQUFDLEVBQUU7S0FDbkIsQ0FBQyxDQUFDLENBQUM7QUFDTixDQUFDO0FBRUQ7Ozt5Q0FHeUM7QUFDekMsTUFBTSxVQUFVLGdCQUFnQixDQUFDLE1BQXFCO0lBQ3BELE1BQU0sTUFBTSxHQUFHLElBQUksR0FBRyxFQUFpQyxDQUFDO0lBQ3hELEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxFQUFFLENBQUM7UUFDM0IsSUFBSSxLQUFLLEVBQUUsSUFBSSxLQUFLLHNCQUFzQjtZQUFFLFNBQVM7UUFDckQsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQztRQUM5QixJQUFJLENBQUMsT0FBTyxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUksT0FBTyxPQUFPLENBQUMsUUFBUSxLQUFLLFFBQVE7WUFBRSxTQUFTO1FBQ3RHLE1BQU0sQ0FBQyxHQUFHLENBQUMsR0FBRyxLQUFLLENBQUMsWUFBWSxJQUFJLE9BQU8sQ0FBQyxRQUFRLEVBQUUsRUFBRTtZQUN0RCwwR0FBMEc7WUFDMUcsMkdBQTJHO1lBQzNHLG1HQUFtRztZQUNuRyxPQUFPLEVBQUUsS0FBSyxDQUFDLFlBQXNCO1lBQ3JDLFFBQVEsRUFBRSxNQUFNLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQztZQUNsQyxlQUFlLEVBQUUsT0FBTyxDQUFDLFFBQVE7WUFDakMsVUFBVSxFQUFFLEtBQUssQ0FBQyxTQUFTO1NBQzVCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFDRCxPQUFPLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztBQUM5QixDQUFDO0FBRUQsU0FBUyxnQkFBZ0IsQ0FBQyxNQUF5QjtJQUNqRCxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxDQUFDO1FBQ3RCLE9BQU8sQ0FBQyxHQUFHLENBQUMsNEZBQTRGLENBQUMsQ0FBQztRQUMxRyxPQUFPO0lBQ1QsQ0FBQztJQUNELEtBQUssTUFBTSxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDO1FBQzlCLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxjQUFjLEtBQUssSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsY0FBYyxHQUFHLEdBQUcsQ0FBQyxHQUFHLENBQUM7UUFDL0YsTUFBTSxLQUFLLEdBQUcsR0FBRyxDQUFDLGNBQWMsS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEdBQUcsR0FBRyxDQUFDLEdBQUcsQ0FBQztRQUMvRixPQUFPLENBQUMsR0FBRyxDQUNULEdBQUcsR0FBRyxDQUFDLE9BQU8sS0FBSyxHQUFHLENBQUMsT0FBTyxhQUFhO1lBQ3pDLFNBQVMsR0FBRyxDQUFDLGNBQWMsSUFBSSxHQUFHLENBQUMsVUFBVSxLQUFLLEtBQUssTUFBTTtZQUM3RCxTQUFTLEdBQUcsQ0FBQyxjQUFjLElBQUksR0FBRyxDQUFDLFVBQVUsS0FBSyxLQUFLLFlBQVksR0FBRyxDQUFDLElBQUksRUFBRSxDQUNoRixDQUFDO0lBQ0osQ0FBQztBQUNILENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLGlCQUFpQixDQUFDLE9BQWlCLEVBQUUsRUFBRSxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMxRyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3JDLDJHQUEyRztJQUMzRyw2R0FBNkc7SUFDN0csNkZBQTZGO0lBQzdGLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQztJQUN6RCxJQUFJLE9BQU8sRUFBRSxDQUFDO1FBQ1osT0FBTyxnQkFBZ0IsQ0FBQyxJQUFJLEVBQUUsbUJBQW1CLE9BQU8sS0FBSyxpQkFBaUIsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQ3ZGLENBQUM7SUFFRCxJQUFJLGVBQWUsQ0FBQztJQUNwQixJQUFJLFdBQVcsQ0FBQztJQUNoQixJQUFJLENBQUM7UUFDSCxlQUFlLEdBQUcsb0JBQW9CLENBQUMsNkJBQTZCLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztRQUMzRSxXQUFXLEdBQUcsZUFBZSxDQUFDLHdCQUF3QixDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDN0QsTUFBTSxNQUFNLEdBQUcsc0JBQXNCLENBQ25DLG1CQUFtQixDQUFDLGVBQWUsQ0FBQyxlQUFlLEVBQUUsQ0FBQyxFQUN0RCxnQkFBZ0IsQ0FBQyxXQUFXLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FDM0MsQ0FBQztRQUNGLElBQUksSUFBSTtZQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7O1lBQ2xELGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzlCLE9BQU8sQ0FBQyxDQUFDO0lBQ1gsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixPQUFPLGdCQUFnQixDQUFDLElBQUksRUFBRSxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0lBQ3pELENBQUM7WUFBUyxDQUFDO1FBQ1QsZUFBZSxFQUFFLEtBQUssRUFBRSxDQUFDO1FBQ3pCLFdBQVcsRUFBRSxLQUFLLEVBQUUsQ0FBQztJQUN2QixDQUFDO0FBQ0gsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/calibration-run.d.ts b/packages/loopover-miner/lib/calibration-run.d.ts deleted file mode 100644 index c8c9ff99c6..0000000000 --- a/packages/loopover-miner/lib/calibration-run.d.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { HistoricalReplayCalibrationInput, Phase7CalibrationConfig, Phase7CalibrationLoopResult, Phase7CalibrationManifest, PrOutcomeCalibrationInput, ReplayHarnessStatus } from "@loopover/engine"; -import type { ObjectiveAnchorResult, ReplayPlanInput, RevealedHistoryEntry } from "./replay-objective-anchor.js"; -import type { AppendEventInput, LedgerEntry } from "./event-ledger.js"; -/** Event-ledger vocabulary for a persisted Phase 7 calibration snapshot (mirrors MINER_PR_OUTCOME_EVENT). */ -export declare const MINER_CALIBRATION_SNAPSHOT_EVENT = "calibration_snapshot"; -/** One completed replay-run task result: what the replay targeted, and the revealed post-T history to score it. */ -export interface ReplayTaskResult { - replayPlan?: ReplayPlanInput | null; - revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null; -} -export interface ScoreCompositeOptions { - computeObjectiveAnchor?: (input: { - replayPlan?: ReplayPlanInput | null; - revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null; - }) => ObjectiveAnchorResult; -} -export interface HistoricalReplayCompositeScore { - compositeScore: number | null; - sampleSize: number; - scores: number[]; -} -/** A completed replay run's descriptor: its per-task results plus the run's identity/freshness/harness health. */ -export interface ReplayRunDescriptor { - replayResults?: readonly ReplayTaskResult[] | null; - replayRunId?: string; - observedAt?: string; - harnessStatus?: ReplayHarnessStatus; -} -export interface BuiltHistoricalReplayInput { - historicalReplay: HistoricalReplayCalibrationInput | null; - compositeScore: number | null; - sampleSize: number; - scores: number[]; -} -/** The persisted, public-safe projection of a Phase7CalibrationLoopResult. */ -export interface CalibrationSnapshotPayload { - enabled: boolean; - combinedAccuracy: number | null; - baselineAccuracy: number; - deltaFromBaseline: number | null; - autonomyIncreasePermitted: boolean; - replayHarnessHold: boolean; - replayHarnessStatus: string; - replayRunDue: boolean; - holdReasons: string[]; - contributingSources: string[]; - replayRunId: string | null; - observedAt: string | null; - replaySampleSize: number; -} -export interface SnapshotMeta { - replayRunId?: string | null; - observedAt?: string | null; - sampleSize?: number; -} -export interface RecordCalibrationSnapshotOptions { - /** Optional at the type level so a caller can pass an unusable ledger to exercise the fail-closed guard; the - * writer throws `invalid_event_ledger` at runtime when this is absent or lacks `appendEvent`. */ - eventLedger?: { - appendEvent(event: AppendEventInput): LedgerEntry; - }; - repoFullName?: string; -} -export interface CalibrationSnapshotReader { - readEvents(filter?: { - since?: number | null; - repoFullName?: string | null; - }): unknown[]; -} -export interface CalibrationSnapshotFilter { - since?: number | null; - repoFullName?: string | null; -} -export interface PersistedCalibrationSnapshot extends CalibrationSnapshotPayload { - repoFullName: string | null; - seq: number | null; - createdAt: string | null; -} -export interface RunCalibrationCycleInput { - config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record | null; - prOutcome?: PrOutcomeCalibrationInput | null; - replayRun?: ReplayRunDescriptor | null; - now?: string | Date | null; - observedAt?: string | null; - repoFullName?: string; -} -export interface RunCalibrationCycleDeps extends ScoreCompositeOptions { - computeLoop?: (input: { - config?: Phase7CalibrationConfig | Phase7CalibrationManifest | Record | null; - prOutcome?: PrOutcomeCalibrationInput | null; - historicalReplay?: HistoricalReplayCalibrationInput | null; - now?: string | Date | null; - }) => Phase7CalibrationLoopResult; - eventLedger?: { - appendEvent(event: AppendEventInput): LedgerEntry; - }; -} -export interface RunCalibrationCycleResult { - result: Phase7CalibrationLoopResult; - snapshot: CalibrationSnapshotPayload; - recorded: LedgerEntry | null; - historicalReplay: HistoricalReplayCalibrationInput | null; - compositeScore: number | null; - sampleSize: number; - scores: number[]; -} -/** - * Score a completed replay run's per-task results with the deterministic objective-anchor scorer and reduce them to - * one composite `[0, 1]` accuracy (the mean of the per-task scores). `replayResults` is a list of - * `{ replayPlan, revealedHistory }` pairs; each non-object entry is defensively skipped. Returns `compositeScore: - * null` (never a fabricated 0) when there is no scorable task. Pure aside from the injected scorer. - */ -export declare function scoreHistoricalReplayComposite(replayResults: readonly ReplayTaskResult[] | null | undefined, options?: ScoreCompositeOptions): HistoricalReplayCompositeScore; -/** - * Build the engine's `HistoricalReplayCalibrationInput` from a replay run descriptor - * (`{ replayResults, replayRunId, observedAt, harnessStatus }`). Returns `historicalReplay: null` when no run - * descriptor is supplied (the engine then holds `no_historical_replay_signal` when the loop is enabled). When a run - * IS supplied its `harnessStatus` flows through verbatim so a degraded/unavailable harness still reaches the - * engine's fail-closed hold path even if it scored zero tasks; a null composite becomes `0` only for the engine's - * numeric contract (the un-fabricated `compositeScore`/`sampleSize` are returned alongside for the snapshot). - */ -export declare function buildHistoricalReplayCalibrationInput(replayRun: ReplayRunDescriptor | null | undefined, options?: ScoreCompositeOptions): BuiltHistoricalReplayInput; -/** - * Derive a JSON-safe, public-safe snapshot payload from a computed `Phase7CalibrationLoopResult`. Only accuracies, - * the documented baseline, hold-reason CODES, and provenance are surfaced -- never raw replay scores or rewards. - * Every field is a number/null, boolean, string/null, or string[] so it round-trips through the event ledger's - * verbatim-JSON serializer unchanged. - */ -export declare function snapshotPayloadFromResult(result: Phase7CalibrationLoopResult, meta?: SnapshotMeta): CalibrationSnapshotPayload; -/** - * Validate + normalize a calibration-snapshot payload, returning `null` on any malformed shape (mirrors - * pr-outcome.js's `normalizePrOutcomePayload`, so a corrupted row can neither be written nor read back). Skipped - * rows are dropped by the reader rather than throwing. - */ -export declare function normalizeCalibrationSnapshotPayload(payload: unknown): CalibrationSnapshotPayload | null; -/** - * Persist one calibration snapshot to an INJECTED event ledger (same dependency-injection shape as pr-outcome.js's - * `recordPrOutcomeSnapshot`, so it's unit-testable without a real SQLite file). Fail-soft: a malformed payload - * returns `null` without appending. An unusable ledger is the only hard error (a programmer wiring mistake). - */ -export declare function recordCalibrationSnapshot(input: unknown, options?: RecordCalibrationSnapshotOptions): LedgerEntry | null; -/** - * Read every persisted calibration snapshot from the injected ledger's ascending append-only stream (mirrors - * pr-outcome.js's `readPrOutcomes`). Foreign event types and malformed payloads are skipped; a ledger that cannot - * read reduces to an empty list. Returns snapshots in ledger order (oldest first). - */ -export declare function readCalibrationSnapshots(eventLedger: CalibrationSnapshotReader, filter?: CalibrationSnapshotFilter): PersistedCalibrationSnapshot[]; -/** The most recent persisted calibration snapshot, or `null` when none exist. */ -export declare function latestCalibrationSnapshot(eventLedger: CalibrationSnapshotReader, filter?: CalibrationSnapshotFilter): PersistedCalibrationSnapshot | null; -/** - * The runner. Scores the replay run (via the objective-anchor scorer), calls the engine's calibration combine with - * the resulting historical-replay composite plus the existing pr_outcome signal, and -- when an event ledger is - * injected -- persists the combined snapshot. Returns the engine result, the derived snapshot payload, the recorded - * ledger entry (or null when no ledger was injected or the payload was malformed), and the un-fabricated - * composite/sample provenance. The engine combine (`computeLoop`) is injectable so unit tests can pin it. - */ -export declare function runHistoricalReplayCalibrationCycle(input?: RunCalibrationCycleInput, deps?: RunCalibrationCycleDeps): RunCalibrationCycleResult; diff --git a/packages/loopover-miner/lib/calibration-run.js b/packages/loopover-miner/lib/calibration-run.js deleted file mode 100644 index 10ef7e03c4..0000000000 --- a/packages/loopover-miner/lib/calibration-run.js +++ /dev/null @@ -1,231 +0,0 @@ -// Phase 7 calibration runner (#4248): the miner-side runner that finally CONNECTS the two finished-but-unwired -// halves #3014 left apart. #3014 landed the engine's pure calibration *combine* contract -// (`computePhase7CalibrationLoop`, packages/loopover-engine/src/phase7-calibration-loop.ts) and #3012 landed the -// deterministic replay *scorer* (`computeObjectiveAnchor`, ./replay-objective-anchor.js), but nothing ever called -// one with the other -- #3014's issue claimed "wired" while only the engine side shipped. This module is the -// missing runner: it scores a completed historical-replay run with the objective-anchor scorer, folds the -// resulting composite into the `HistoricalReplayCalibrationInput` shape the engine expects, calls the combine with -// the existing pr_outcome signal, and PERSISTS the combined snapshot to the local append-only event ledger (a typed -// event layered on event-ledger.js exactly like pr-outcome.js's MINER_PR_OUTCOME_EVENT), queryable via -// `loopover-miner ledger list --type calibration_snapshot`. -// -// SCOPE: this runner is read/measure-only. It produces and persists the tracked calibration metric; it NEVER acts -// on it (no autonomy-level bump, no gate-threshold tune) -- that enforcement is maintainer-only and fail-closed -// (see docs/miner-selfimprove-calibration.md's maintainer-only boundary). The engine owns the deterministic -// combine/freshness/threshold/hold-reason logic; this module owns scheduling the score and persisting the row. -import { computePhase7CalibrationLoop } from "@loopover/engine"; -import { computeObjectiveAnchor } from "./replay-objective-anchor.js"; -/** Event-ledger vocabulary for a persisted Phase 7 calibration snapshot (mirrors MINER_PR_OUTCOME_EVENT). */ -export const MINER_CALIBRATION_SNAPSHOT_EVENT = "calibration_snapshot"; -const SCORE_PRECISION = 1e6; -function roundScore(value) { - return Math.round(Math.min(1, Math.max(0, value)) * SCORE_PRECISION) / SCORE_PRECISION; -} -function isFiniteNumber(value) { - return typeof value === "number" && Number.isFinite(value); -} -function numberOrNull(value) { - return isFiniteNumber(value) ? value : null; -} -function optionalString(value) { - if (typeof value !== "string") - return null; - const trimmed = value.trim(); - return trimmed || null; -} -/** - * Score a completed replay run's per-task results with the deterministic objective-anchor scorer and reduce them to - * one composite `[0, 1]` accuracy (the mean of the per-task scores). `replayResults` is a list of - * `{ replayPlan, revealedHistory }` pairs; each non-object entry is defensively skipped. Returns `compositeScore: - * null` (never a fabricated 0) when there is no scorable task. Pure aside from the injected scorer. - */ -export function scoreHistoricalReplayComposite(replayResults, options = {}) { - const scoreOne = options.computeObjectiveAnchor ?? computeObjectiveAnchor; - const list = Array.isArray(replayResults) ? replayResults : []; - const scores = []; - for (const entry of list) { - if (!entry || typeof entry !== "object" || Array.isArray(entry)) - continue; - const { score } = scoreOne({ replayPlan: entry.replayPlan, revealedHistory: entry.revealedHistory }); - if (isFiniteNumber(score)) - scores.push(score); - } - const sampleSize = scores.length; - const compositeScore = sampleSize === 0 ? null : roundScore(scores.reduce((sum, s) => sum + s, 0) / sampleSize); - return { compositeScore, sampleSize, scores }; -} -/** - * Build the engine's `HistoricalReplayCalibrationInput` from a replay run descriptor - * (`{ replayResults, replayRunId, observedAt, harnessStatus }`). Returns `historicalReplay: null` when no run - * descriptor is supplied (the engine then holds `no_historical_replay_signal` when the loop is enabled). When a run - * IS supplied its `harnessStatus` flows through verbatim so a degraded/unavailable harness still reaches the - * engine's fail-closed hold path even if it scored zero tasks; a null composite becomes `0` only for the engine's - * numeric contract (the un-fabricated `compositeScore`/`sampleSize` are returned alongside for the snapshot). - */ -export function buildHistoricalReplayCalibrationInput(replayRun, options = {}) { - if (!replayRun || typeof replayRun !== "object" || Array.isArray(replayRun)) { - return { historicalReplay: null, compositeScore: null, sampleSize: 0, scores: [] }; - } - const composite = scoreHistoricalReplayComposite(replayRun.replayResults, options); - return { - historicalReplay: { - compositeScore: composite.compositeScore ?? 0, - replayRunId: replayRun.replayRunId, - observedAt: replayRun.observedAt, - harnessStatus: replayRun.harnessStatus, - }, - compositeScore: composite.compositeScore, - sampleSize: composite.sampleSize, - scores: composite.scores, - }; -} -/** - * Derive a JSON-safe, public-safe snapshot payload from a computed `Phase7CalibrationLoopResult`. Only accuracies, - * the documented baseline, hold-reason CODES, and provenance are surfaced -- never raw replay scores or rewards. - * Every field is a number/null, boolean, string/null, or string[] so it round-trips through the event ledger's - * verbatim-JSON serializer unchanged. - */ -export function snapshotPayloadFromResult(result, meta = {}) { - return { - enabled: result.enabled === true, - combinedAccuracy: numberOrNull(result.combinedAccuracy), - baselineAccuracy: isFiniteNumber(result.baselineAccuracy) ? result.baselineAccuracy : 0, - deltaFromBaseline: numberOrNull(result.deltaFromBaseline), - autonomyIncreasePermitted: result.autonomyIncreasePermitted === true, - replayHarnessHold: result.replayHarnessHold === true, - replayHarnessStatus: optionalString(result.replayHarnessStatus) ?? "missing", - replayRunDue: result.replayRunDue === true, - holdReasons: Array.isArray(result.holdReasons) ? result.holdReasons.map(String) : [], - contributingSources: Array.isArray(result.audit?.contributingSources) - ? result.audit.contributingSources.map(String) - : [], - replayRunId: optionalString(meta.replayRunId), - observedAt: optionalString(meta.observedAt), - replaySampleSize: Number.isInteger(meta.sampleSize) && meta.sampleSize >= 0 ? meta.sampleSize : 0, - }; -} -/** - * Validate + normalize a calibration-snapshot payload, returning `null` on any malformed shape (mirrors - * pr-outcome.js's `normalizePrOutcomePayload`, so a corrupted row can neither be written nor read back). Skipped - * rows are dropped by the reader rather than throwing. - */ -export function normalizeCalibrationSnapshotPayload(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return null; - const record = payload; - if (record.combinedAccuracy !== null && !isFiniteNumber(record.combinedAccuracy)) - return null; - if (!isFiniteNumber(record.baselineAccuracy)) - return null; - if (record.deltaFromBaseline !== null && !isFiniteNumber(record.deltaFromBaseline)) - return null; - if (typeof record.autonomyIncreasePermitted !== "boolean") - return null; - const replayHarnessStatus = optionalString(record.replayHarnessStatus); - if (!replayHarnessStatus) - return null; - if (!Array.isArray(record.holdReasons) || record.holdReasons.some((code) => typeof code !== "string")) { - return null; - } - const contributingSources = Array.isArray(record.contributingSources) - ? record.contributingSources.filter((code) => typeof code === "string") - : []; - return { - enabled: record.enabled === true, - combinedAccuracy: record.combinedAccuracy, - baselineAccuracy: record.baselineAccuracy, - deltaFromBaseline: record.deltaFromBaseline, - autonomyIncreasePermitted: record.autonomyIncreasePermitted, - replayHarnessHold: record.replayHarnessHold === true, - replayHarnessStatus, - replayRunDue: record.replayRunDue === true, - holdReasons: record.holdReasons, - contributingSources, - replayRunId: optionalString(record.replayRunId), - observedAt: optionalString(record.observedAt), - replaySampleSize: Number.isInteger(record.replaySampleSize) && record.replaySampleSize >= 0 ? record.replaySampleSize : 0, - }; -} -/** - * Persist one calibration snapshot to an INJECTED event ledger (same dependency-injection shape as pr-outcome.js's - * `recordPrOutcomeSnapshot`, so it's unit-testable without a real SQLite file). Fail-soft: a malformed payload - * returns `null` without appending. An unusable ledger is the only hard error (a programmer wiring mistake). - */ -export function recordCalibrationSnapshot(input, options = {}) { - const eventLedger = options.eventLedger; - if (!eventLedger || typeof eventLedger.appendEvent !== "function") - throw new Error("invalid_event_ledger"); - const payload = normalizeCalibrationSnapshotPayload(input); - if (!payload) - return null; - const repoFullName = optionalString(options.repoFullName); - return eventLedger.appendEvent({ - type: MINER_CALIBRATION_SNAPSHOT_EVENT, - ...(repoFullName ? { repoFullName } : {}), - payload, - }); -} -/** - * Read every persisted calibration snapshot from the injected ledger's ascending append-only stream (mirrors - * pr-outcome.js's `readPrOutcomes`). Foreign event types and malformed payloads are skipped; a ledger that cannot - * read reduces to an empty list. Returns snapshots in ledger order (oldest first). - */ -export function readCalibrationSnapshots(eventLedger, filter = {}) { - const events = eventLedger && typeof eventLedger.readEvents === "function" ? eventLedger.readEvents(filter) : []; - const snapshots = []; - for (const event of Array.isArray(events) ? events : []) { - const record = event; - if (record?.type !== MINER_CALIBRATION_SNAPSHOT_EVENT) - continue; - const normalized = normalizeCalibrationSnapshotPayload(record.payload); - if (!normalized) - continue; - snapshots.push({ - ...normalized, - repoFullName: typeof record.repoFullName === "string" ? record.repoFullName : null, - seq: Number.isInteger(record.seq) ? record.seq : null, - createdAt: optionalString(record.createdAt), - }); - } - return snapshots; -} -/** The most recent persisted calibration snapshot, or `null` when none exist. */ -export function latestCalibrationSnapshot(eventLedger, filter = {}) { - const snapshots = readCalibrationSnapshots(eventLedger, filter); - return snapshots.length > 0 ? snapshots[snapshots.length - 1] : null; -} -/** - * The runner. Scores the replay run (via the objective-anchor scorer), calls the engine's calibration combine with - * the resulting historical-replay composite plus the existing pr_outcome signal, and -- when an event ledger is - * injected -- persists the combined snapshot. Returns the engine result, the derived snapshot payload, the recorded - * ledger entry (or null when no ledger was injected or the payload was malformed), and the un-fabricated - * composite/sample provenance. The engine combine (`computeLoop`) is injectable so unit tests can pin it. - */ -export function runHistoricalReplayCalibrationCycle(input = {}, deps = {}) { - const computeLoop = deps.computeLoop ?? computePhase7CalibrationLoop; - const built = buildHistoricalReplayCalibrationInput(input.replayRun, deps); - const result = computeLoop({ - ...(input.config !== undefined ? { config: input.config } : {}), - ...(input.prOutcome !== undefined ? { prOutcome: input.prOutcome } : {}), - historicalReplay: built.historicalReplay, - ...(input.now !== undefined ? { now: input.now } : {}), - }); - const snapshot = snapshotPayloadFromResult(result, { - replayRunId: built.historicalReplay?.replayRunId ?? null, - observedAt: input.observedAt ?? built.historicalReplay?.observedAt ?? null, - sampleSize: built.sampleSize, - }); - const recorded = deps.eventLedger - ? recordCalibrationSnapshot(snapshot, { eventLedger: deps.eventLedger, repoFullName: input.repoFullName }) - : null; - return { - result, - snapshot, - recorded, - historicalReplay: built.historicalReplay, - compositeScore: built.compositeScore, - sampleSize: built.sampleSize, - scores: built.scores, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsaWJyYXRpb24tcnVuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2FsaWJyYXRpb24tcnVuLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtHQUErRztBQUMvRyx5RkFBeUY7QUFDekYsaUhBQWlIO0FBQ2pILGtIQUFrSDtBQUNsSCw2R0FBNkc7QUFDN0csMEdBQTBHO0FBQzFHLG1IQUFtSDtBQUNuSCxvSEFBb0g7QUFDcEgsdUdBQXVHO0FBQ3ZHLDREQUE0RDtBQUM1RCxFQUFFO0FBQ0Ysa0hBQWtIO0FBQ2xILGdIQUFnSDtBQUNoSCw0R0FBNEc7QUFDNUcsK0dBQStHO0FBRS9HLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBU2hFLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBUXRFLDZHQUE2RztBQUM3RyxNQUFNLENBQUMsTUFBTSxnQ0FBZ0MsR0FBRyxzQkFBc0IsQ0FBQztBQTZHdkUsTUFBTSxlQUFlLEdBQUcsR0FBRyxDQUFDO0FBRTVCLFNBQVMsVUFBVSxDQUFDLEtBQWE7SUFDL0IsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLEtBQUssQ0FBQyxDQUFDLEdBQUcsZUFBZSxDQUFDLEdBQUcsZUFBZSxDQUFDO0FBQ3pGLENBQUM7QUFFRCxTQUFTLGNBQWMsQ0FBQyxLQUFjO0lBQ3BDLE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDN0QsQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEtBQWM7SUFDbEMsT0FBTyxjQUFjLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0FBQzlDLENBQUM7QUFFRCxTQUFTLGNBQWMsQ0FBQyxLQUFj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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/calibration-types.d.ts b/packages/loopover-miner/lib/calibration-types.d.ts deleted file mode 100644 index 2aee19a1d5..0000000000 --- a/packages/loopover-miner/lib/calibration-types.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** A single gate-prediction row the miner will replay against observed outcomes. */ -export type PredictedVerdictRecord = { - targetId: string; - project: string; - predictedDecision: string; - recordedAt: string; - source?: string; -}; -/** The realized human outcome for a previously predicted target. */ -export type ObservedOutcomeRecord = { - targetId: string; - project: string; - outcomeDecision: string; - recordedAt: string; -}; -/** Per-project confusion-matrix row — field names mirror `GateEvalRow` in `src/review/parity.ts`. */ -export type CalibrationRow = { - project: string; - wouldMerge: number; - mergeConfirmed: number; - mergeFalse: number; - wouldClose: number; - closeConfirmed: number; - closeFalse: number; - hold: number; - decided: number; - mergePrecision: number | null; - closePrecision: number | null; -}; -/** Aggregate calibration report over one or more projects. */ -export type CalibrationReport = { - rows: CalibrationRow[]; - /** True once at least one project has enough decided samples to read meaningfully. */ - hasSignal: boolean; -}; -export declare function isPredictedVerdictRecord(value: unknown): value is PredictedVerdictRecord; -export declare function isObservedOutcomeRecord(value: unknown): value is ObservedOutcomeRecord; -export declare function isCalibrationRow(value: unknown): value is CalibrationRow; -export declare function isCalibrationReport(value: unknown): value is CalibrationReport; diff --git a/packages/loopover-miner/lib/calibration-types.js b/packages/loopover-miner/lib/calibration-types.js deleted file mode 100644 index a80b60b496..0000000000 --- a/packages/loopover-miner/lib/calibration-types.js +++ /dev/null @@ -1,59 +0,0 @@ -// Shared calibration shapes for the miner self-improvement phase (#2332). Types-only scaffolding — -// report/ledger/metrics issues build on this module. Field names mirror `GateEvalRow` / -// `GateEvalReport` in `src/review/parity.ts` for easy mental mapping without importing cloud code. -function isNonEmptyString(value) { - return typeof value === "string" && value.trim().length > 0; -} -function isOptionalString(value) { - return value === undefined || isNonEmptyString(value); -} -function isNonNegativeInteger(value) { - return typeof value === "number" && Number.isInteger(value) && value >= 0; -} -function isNullableRatio(value) { - return value === null || (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1); -} -export function isPredictedVerdictRecord(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) - return false; - const record = value; - return (isNonEmptyString(record.targetId) - && isNonEmptyString(record.project) - && isNonEmptyString(record.predictedDecision) - && isNonEmptyString(record.recordedAt) - && isOptionalString(record.source)); -} -export function isObservedOutcomeRecord(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) - return false; - const record = value; - return (isNonEmptyString(record.targetId) - && isNonEmptyString(record.project) - && isNonEmptyString(record.outcomeDecision) - && isNonEmptyString(record.recordedAt)); -} -export function isCalibrationRow(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) - return false; - const row = value; - return (isNonEmptyString(row.project) - && isNonNegativeInteger(row.wouldMerge) - && isNonNegativeInteger(row.mergeConfirmed) - && isNonNegativeInteger(row.mergeFalse) - && isNonNegativeInteger(row.wouldClose) - && isNonNegativeInteger(row.closeConfirmed) - && isNonNegativeInteger(row.closeFalse) - && isNonNegativeInteger(row.hold) - && isNonNegativeInteger(row.decided) - && isNullableRatio(row.mergePrecision) - && isNullableRatio(row.closePrecision)); -} -export function isCalibrationReport(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) - return false; - const report = value; - return (typeof report.hasSignal === "boolean" - && Array.isArray(report.rows) - && report.rows.every((row) => isCalibrationRow(row))); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsaWJyYXRpb24tdHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjYWxpYnJhdGlvbi10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxtR0FBbUc7QUFDbkcsd0ZBQXdGO0FBQ3hGLG1HQUFtRztBQXlDbkcsU0FBUyxnQkFBZ0IsQ0FBQyxLQUFjO0lBQ3RDLE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDO0FBQzlELENBQUM7QUFFRCxTQUFTLGdCQUFnQixDQUFDLEtBQWM7SUFDdEMsT0FBTyxLQUFLLEtBQUssU0FBUyxJQUFJLGdCQUFnQixDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3hELENBQUM7QUFFRCxTQUFTLG9CQUFvQixDQUFDLEtBQWM7SUFDMUMsT0FBTyxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLElBQUksQ0FBQyxDQUFDO0FBQzVFLENBQUM7QUFFRCxTQUFTLGVBQWUsQ0FBQyxLQUFjO0lBQ3JDLE9BQU8sS0FBSyxLQUFLLElBQUksSUFBSSxDQUFDLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUssSUFBSSxDQUFDLElBQUksS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDO0FBQzdHLENBQUM7QUFFRCxNQUFNLFVBQVUsd0JBQXdCLENBQUMsS0FBYztJQUNyRCxJQUFJLEtBQUssS0FBSyxJQUFJLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDdEYsTUFBTSxNQUFNLEdBQUcsS0FBZ0MsQ0FBQztJQUNoRCxPQUFPLENBQ0wsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQztXQUM5QixnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDO1dBQ2hDLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxpQkFBaUIsQ0FBQztXQUMxQyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDO1dBQ25DLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FDbkMsQ0FBQztBQUNKLENBQUM7QUFFRCxNQUFNLFVBQVUsdUJBQXVCLENBQUMsS0FBYztJQUNwRCxJQUFJLEtBQUssS0FBSyxJQUFJLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDdEYsTUFBTSxNQUFNLEdBQUcsS0FBZ0MsQ0FBQztJQUNoRCxPQUFPLENBQ0wsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQztXQUM5QixnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDO1dBQ2hDLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUM7V0FDeEMsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUN2QyxDQUFDO0FBQ0osQ0FBQztBQUVELE1BQU0sVUFBVSxnQkFBZ0IsQ0FBQyxLQUFjO0lBQzdDLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUN0RixNQUFNLEdBQUcsR0FBRyxLQUFnQyxDQUFDO0lBQzdDLE9BQU8sQ0FDTCxnQkFBZ0IsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDO1dBQzFCLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUM7V0FDcEMsb0JBQW9CLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQztXQUN4QyxvQkFBb0IsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDO1dBQ3BDLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUM7V0FDcEMsb0JBQW9CLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQztXQUN4QyxvQkFBb0IsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDO1dBQ3BDLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUM7V0FDOUIsb0JBQW9CLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQztXQUNqQyxlQUFlLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQztXQUNuQyxlQUFlLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxDQUN2QyxDQUFDO0FBQ0osQ0FBQztBQUVELE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxLQUFjO0lBQ2hELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUN0RixNQUFNLE1BQU0sR0FBRyxLQUFnQyxDQUFDO0lBQ2hELE9BQU8sQ0FDTCxPQUFPLE1BQU0sQ0FBQyxTQUFTLEtBQUssU0FBUztXQUNsQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUM7V0FDMUIsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQ3JELENBQUM7QUFDSixDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/calibration.d.ts b/packages/loopover-miner/lib/calibration.d.ts deleted file mode 100644 index 67269ef03a..0000000000 --- a/packages/loopover-miner/lib/calibration.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isCalibrationReport, isCalibrationRow, isObservedOutcomeRecord, isPredictedVerdictRecord } from "./calibration-types.js"; -import type { CalibrationReport, ObservedOutcomeRecord, PredictedVerdictRecord } from "./calibration-types.js"; -export type { CalibrationReport, CalibrationRow, ObservedOutcomeRecord, PredictedVerdictRecord, } from "./calibration-types.js"; -export { isCalibrationReport, isCalibrationRow, isObservedOutcomeRecord, isPredictedVerdictRecord }; -/** - * Join predicted-verdict records with realized-outcome records into a per-project calibration report. Pure and - * read-only. A prediction counts as "decided" only when a realized outcome for the SAME `(project, targetId)` - * exists AND resolves to a clear `merge` or `close`; a still-pending prediction (no outcome) or one whose outcome - * is unrecognized is skipped. Per project it tallies the confusion matrix (would-merge/close vs confirmed/false, - * plus holds) and derives merge/close precision (null below one relevant sample). Malformed records on either - * side are ignored. Rows are sorted by project for a stable render. - */ -export declare function buildCalibrationReport(predictions: PredictedVerdictRecord[], outcomes: ObservedOutcomeRecord[]): CalibrationReport; diff --git a/packages/loopover-miner/lib/calibration.js b/packages/loopover-miner/lib/calibration.js deleted file mode 100644 index 6fdbe70121..0000000000 --- a/packages/loopover-miner/lib/calibration.js +++ /dev/null @@ -1,96 +0,0 @@ -// Calibration report: join the miner's own predicted gate verdicts with the realized outcomes it later observed -// (#4849). Read-only aggregation only — it never touches the live scoring/calibration logic that feeds the gate -// (maintainer-owned). Builds on the types-only scaffolding in calibration-types.js. -import { isCalibrationReport, isCalibrationRow, isObservedOutcomeRecord, isPredictedVerdictRecord, } from "./calibration-types.js"; -export { isCalibrationReport, isCalibrationRow, isObservedOutcomeRecord, isPredictedVerdictRecord }; -/** Normalize a decision string to the calibration vocabulary (`merge` / `close` / `hold`), or `""` when it is - * unrecognized. `value` is always the already-validated non-empty string field of a record (the type guards run - * first), so no non-string handling is needed here. Accepts both the predicted (`merge`/`close`/`hold`) and the - * realized (`merged`/`closed`) forms. */ -function normalizeDecision(value) { - const decision = value.trim().toLowerCase(); - if (decision === "merge" || decision === "merged") - return "merge"; - if (decision === "close" || decision === "closed") - return "close"; - if (decision === "hold") - return "hold"; - return ""; -} -function emptyRow(project) { - return { - project, - wouldMerge: 0, - mergeConfirmed: 0, - mergeFalse: 0, - wouldClose: 0, - closeConfirmed: 0, - closeFalse: 0, - hold: 0, - decided: 0, - mergePrecision: null, - closePrecision: null, - }; -} -// Key a record by its (project, targetId). Project and targetId are validated non-empty strings; the space -// separator is fine for keying (collisions across different (project, targetId) pairs are astronomically -// unlikely and would only merge two projects' tallies, never fabricate a false one). -function recordKey(project, targetId) { - return `${project} ${targetId}`; -} -/** - * Join predicted-verdict records with realized-outcome records into a per-project calibration report. Pure and - * read-only. A prediction counts as "decided" only when a realized outcome for the SAME `(project, targetId)` - * exists AND resolves to a clear `merge` or `close`; a still-pending prediction (no outcome) or one whose outcome - * is unrecognized is skipped. Per project it tallies the confusion matrix (would-merge/close vs confirmed/false, - * plus holds) and derives merge/close precision (null below one relevant sample). Malformed records on either - * side are ignored. Rows are sorted by project for a stable render. - */ -export function buildCalibrationReport(predictions, outcomes) { - const outcomeByKey = new Map(); - for (const outcome of Array.isArray(outcomes) ? outcomes : []) { - if (!isObservedOutcomeRecord(outcome)) - continue; - outcomeByKey.set(recordKey(outcome.project, outcome.targetId), normalizeDecision(outcome.outcomeDecision)); - } - const byProject = new Map(); - for (const prediction of Array.isArray(predictions) ? predictions : []) { - if (!isPredictedVerdictRecord(prediction)) - continue; - const observed = outcomeByKey.get(recordKey(prediction.project, prediction.targetId)); - if (observed !== "merge" && observed !== "close") - continue; // pending or unclassifiable outcome - let row = byProject.get(prediction.project); - if (!row) { - row = emptyRow(prediction.project); - byProject.set(prediction.project, row); - } - row.decided += 1; - const predicted = normalizeDecision(prediction.predictedDecision); - if (predicted === "merge") { - row.wouldMerge += 1; - if (observed === "merge") - row.mergeConfirmed += 1; - else - row.mergeFalse += 1; - } - else if (predicted === "close") { - row.wouldClose += 1; - if (observed === "close") - row.closeConfirmed += 1; - else - row.closeFalse += 1; - } - else if (predicted === "hold") { - row.hold += 1; - } - } - const rows = [...byProject.values()].sort((a, b) => a.project.localeCompare(b.project)); - for (const row of rows) { - row.mergePrecision = row.wouldMerge > 0 ? row.mergeConfirmed / row.wouldMerge : null; - row.closePrecision = row.wouldClose > 0 ? row.closeConfirmed / row.wouldClose : null; - } - // Signal exists once any project carries at least one decided (predicted-then-realized) sample. - return { hasSignal: rows.length > 0, rows }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsaWJyYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjYWxpYnJhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxnSEFBZ0g7QUFDaEgsZ0hBQWdIO0FBQ2hILG9GQUFvRjtBQUNwRixPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGdCQUFnQixFQUNoQix1QkFBdUIsRUFDdkIsd0JBQXdCLEdBQ3pCLE1BQU0sd0JBQXdCLENBQUM7QUFlaEMsT0FBTyxFQUFFLG1CQUFtQixFQUFFLGdCQUFnQixFQUFFLHVCQUF1QixFQUFFLHdCQUF3QixFQUFFLENBQUM7QUFFcEc7OzswQ0FHMEM7QUFDMUMsU0FBUyxpQkFBaUIsQ0FBQyxLQUFhO0lBQ3RDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUM1QyxJQUFJLFFBQVEsS0FBSyxPQUFPLElBQUksUUFBUSxLQUFLLFFBQVE7UUFBRSxPQUFPLE9BQU8sQ0FBQztJQUNsRSxJQUFJLFFBQVEsS0FBSyxPQUFPLElBQUksUUFBUSxLQUFLLFFBQVE7UUFBRSxPQUFPLE9BQU8sQ0FBQztJQUNsRSxJQUFJLFFBQVEsS0FBSyxNQUFNO1FBQUUsT0FBTyxNQUFNLENBQUM7SUFDdkMsT0FBTyxFQUFFLENBQUM7QUFDWixDQUFDO0FBRUQsU0FBUyxRQUFRLENBQUMsT0FBZTtJQUMvQixPQUFPO1FBQ0wsT0FBTztRQUNQLFVBQVUsRUFBRSxDQUFDO1FBQ2IsY0FBYyxFQUFFLENBQUM7UUFDakIsVUFBVSxFQUFFLENBQUM7UUFDYixVQUFVLEVBQUUsQ0FBQztRQUNiLGNBQWMsRUFBRSxDQUFDO1FBQ2pCLFVBQVUsRUFBRSxDQUFDO1FBQ2IsSUFBSSxFQUFFLENBQUM7UUFDUCxPQUFPLEVBQUUsQ0FBQztRQUNWLGNBQWMsRUFBRSxJQUFJO1FBQ3BCLGNBQWMsRUFBRSxJQUFJO0tBQ3JCLENBQUM7QUFDSixDQUFDO0FBRUQsMkdBQTJHO0FBQzNHLHlHQUF5RztBQUN6RyxxRkFBcUY7QUFDckYsU0FBUyxTQUFTLENBQUMsT0FBZSxFQUFFLFFBQWdCO0lBQ2xELE9BQU8sR0FBRyxPQUFPLElBQUksUUFBUSxFQUFFLENBQUM7QUFDbEMsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsc0JBQXNCLENBQ3BDLFdBQXFDLEVBQ3JDLFFBQWlDO0lBRWpDLE1BQU0sWUFBWSxHQUFHLElBQUksR0FBRyxFQUEyQyxDQUFDO0lBQ3hFLEtBQUssTUFBTSxPQUFPLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUM5RCxJQUFJLENBQUMsdUJBQXVCLENBQUMsT0FBTyxDQUFDO1lBQUUsU0FBUztRQUNoRCxZQUFZLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxpQkFBaUIsQ0FBQyxPQUFPLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQztJQUM3RyxDQUFDO0lBRUQsTUFBTSxTQUFTLEdBQUcsSUFBSSxHQUFHLEVBQTBCLENBQUM7SUFDcEQsS0FBSyxNQUFNLFVBQVUsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDO1FBQ3ZFLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxVQUFVLENBQUM7WUFBRSxTQUFTO1FBQ3BELE1BQU0sUUFBUSxHQUFHLFlBQVksQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7UUFDdEYsSUFBSSxRQUFRLEtBQUssT0FBTyxJQUFJLFFBQVEsS0FBSyxPQUFPO1lBQUUsU0FBUyxDQUFDLG9DQUFvQztRQUNoRyxJQUFJLEdBQUcsR0FBRyxTQUFTLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUM1QyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDVCxHQUFHLEdBQUcsUUFBUSxDQUFDLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUNuQyxTQUFTLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDekMsQ0FBQztRQUNELEdBQUcsQ0FBQyxPQUFPLElBQUksQ0FBQyxDQUFDO1FBQ2pCLE1BQU0sU0FBUyxHQUFHLGlCQUFpQixDQUFDLFVBQVUsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1FBQ2xFLElBQUksU0FBUyxLQUFLLE9BQU8sRUFBRSxDQUFDO1lBQzFCLEdBQUcsQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDO1lBQ3BCLElBQUksUUFBUSxLQUFLLE9BQU87Z0JBQUUsR0FBRyxDQUFDLGNBQWMsSUFBSSxDQUFDLENBQUM7O2dCQUM3QyxHQUFHLENBQUMsVUFBVSxJQUFJLENBQUMsQ0FBQztRQUMzQixDQUFDO2FBQU0sSUFBSSxTQUFTLEtBQUssT0FBTyxFQUFFLENBQUM7WUFDakMsR0FBRyxDQUFDLFVBQVUsSUFBSSxDQUFDLENBQUM7WUFDcEIsSUFBSSxRQUFRLEtBQUssT0FBTztnQkFBRSxHQUFHLENBQUMsY0FBYyxJQUFJLENBQUMsQ0FBQzs7Z0JBQzdDLEdBQUcsQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDO1FBQzNCLENBQUM7YUFBTSxJQUFJLFNBQVMsS0FBSyxNQUFNLEVBQUUsQ0FBQztZQUNoQyxHQUFHLENBQUMsSUFBSSxJQUFJLENBQUMsQ0FBQztRQUNoQixDQUFDO0lBQ0gsQ0FBQztJQUVELE1BQU0sSUFBSSxHQUFHLENBQUMsR0FBRyxTQUFTLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztJQUN4RixLQUFLLE1BQU0sR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDO1FBQ3ZCLEdBQUcsQ0FBQyxjQUFjLEdBQUcsR0FBRyxDQUFDLFVBQVUsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEdBQUcsR0FBRyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ3JGLEdBQUcsQ0FBQyxjQUFjLEdBQUcsR0FBRyxDQUFDLFVBQVUsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEdBQUcsR0FBRyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0lBQ3ZGLENBQUM7SUFDRCxnR0FBZ0c7SUFDaEcsT0FBTyxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUM5QyxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/chat-action-dispatch.d.ts b/packages/loopover-miner/lib/chat-action-dispatch.d.ts deleted file mode 100644 index b1e4ba3d6e..0000000000 --- a/packages/loopover-miner/lib/chat-action-dispatch.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ChatActionRegistry, ChatActionRequest } from "./chat-action-registry.js"; -/** Env var an operator sets to turn the chat-action dispatch layer on. */ -export declare const CHAT_ACTION_DISPATCH_FLAG = "LOOPOVER_MINER_CHAT_ACTIONS"; -/** The one and only value that enables dispatch. Anything else (unset, empty, "true", "1", ...) stays off. */ -export declare const CHAT_ACTION_DISPATCH_ENABLE_VALUE = "enabled"; -/** - * Fail-closed config-flag gate: enabled only when the flag is set to exactly the enable value (trimmed). - * Unset, empty, or any other value -- including truthy-looking ones like "true"/"1" -- reads as disabled. - */ -export declare function isChatActionDispatchEnabled(env?: Record): boolean; -export type ChatActionDispatchResult = { - ok: boolean; - status: string; - action: string | null; - [key: string]: unknown; -}; -/** - * The single entry point every chat-issued action goes through. In order: - * 1. Check the config flag FIRST -- before touching the registry or validating params. When disabled, - * return a clearly-typed `"disabled"` result and look up nothing. - * 2. Reject an unknown (unregistered) action. - * 3. Run the action's own registered params-validator; reject on failure without coercing or dropping - * fields (the caller's `params` is passed through unchanged). - * 4. Invoke the registered (governor-gated) handler and return its result. - */ -export declare function dispatchChatAction(request: ChatActionRequest, options?: { - env?: Record; - registry?: ChatActionRegistry; -}): Promise; diff --git a/packages/loopover-miner/lib/chat-action-dispatch.js b/packages/loopover-miner/lib/chat-action-dispatch.js deleted file mode 100644 index 7605c5e481..0000000000 --- a/packages/loopover-miner/lib/chat-action-dispatch.js +++ /dev/null @@ -1,78 +0,0 @@ -// Chat action-dispatch chokepoint (#6519). -// -// SINGLE ENTRY POINT, NEVER BYPASS: every action a miner-chat message issues MUST go through -// `dispatchChatAction` here -- never a parallel or direct call into a registered handler, an HTTP endpoint, -// or a local-write tool. This function is the one place the config flag is checked and the one place a -// registered handler is looked up and invoked. It adds NO second safety check of its own: the real -// fail-closed enforcement lives in packages/loopover-engine/src/governor/chokepoint.ts (the precedence -// ladder) reached through the packages/loopover-miner/lib/governor-chokepoint.js stateful wrapper, which the -// registry's `governorGatedHandler` contract forces every registered handler through. Dispatch only gates on -// the flag, rejects unknown actions, and runs the registered params-validator before invoking the handler. -// -// Disabled by default: the flag fails closed (off unless explicitly enabled), and the shared registry -// (chat-action-registry.js) ships empty, so no action can execute until a child issue registers a handler -// AND an operator flips the flag on. -import { chatActionRegistry } from "./chat-action-registry.js"; -/** Env var an operator sets to turn the chat-action dispatch layer on. */ -export const CHAT_ACTION_DISPATCH_FLAG = "LOOPOVER_MINER_CHAT_ACTIONS"; -/** The one and only value that enables dispatch. Anything else (unset, empty, "true", "1", ...) stays off. */ -export const CHAT_ACTION_DISPATCH_ENABLE_VALUE = "enabled"; -/** - * Fail-closed config-flag gate: enabled only when the flag is set to exactly the enable value (trimmed). - * Unset, empty, or any other value -- including truthy-looking ones like "true"/"1" -- reads as disabled. - */ -export function isChatActionDispatchEnabled(env = process.env) { - const raw = env?.[CHAT_ACTION_DISPATCH_FLAG]; - return typeof raw === "string" && raw.trim() === CHAT_ACTION_DISPATCH_ENABLE_VALUE; -} -/** - * The single entry point every chat-issued action goes through. In order: - * 1. Check the config flag FIRST -- before touching the registry or validating params. When disabled, - * return a clearly-typed `"disabled"` result and look up nothing. - * 2. Reject an unknown (unregistered) action. - * 3. Run the action's own registered params-validator; reject on failure without coercing or dropping - * fields (the caller's `params` is passed through unchanged). - * 4. Invoke the registered (governor-gated) handler and return its result. - */ -export async function dispatchChatAction(request, options = {}) { - const env = options.env ?? process.env; - // Flag first -- before touching the registry or validating params. Fail closed. - if (!isChatActionDispatchEnabled(env)) { - return { ok: false, status: "disabled", action: readAction(request) }; - } - const registry = options.registry ?? chatActionRegistry; - const action = readAction(request); - if (action === null || !registry.has(action)) { - return { ok: false, status: "unknown_action", action }; - } - const registered = registry.get(action); - let valid; - try { - valid = registered.paramsValidator(request?.params) === true; - } - catch (error) { - // A validator that throws is treated as a rejection (fail closed), not as a dispatch error. - return { ok: false, status: "invalid_params", action, error: error instanceof Error ? error.message : String(error) }; - } - if (!valid) { - return { ok: false, status: "invalid_params", action }; - } - let result; - try { - result = await registered.handler(request); - } - catch { - // A handler that throws fails closed with the module's typed result shape (#6989), consistent with the - // paramsValidator catch above. The thrown value is deliberately NOT echoed back: a handler wraps - // arbitrary action work (e.g. a network call), so its error could carry external detail -- the sibling - // fail-closed paths (sentry.js, pretooluse-hook.js) likewise swallow rather than surface it. A distinct - // "handler_error" status still lets a caller tell an execution failure from a params-validation failure. - return { ok: false, status: "handler_error", action }; - } - return { ok: true, status: "dispatched", action, result }; -} -/** The requested action name, or null when the request omits a string action. */ -function readAction(request) { - return request && typeof request.action === "string" ? request.action : null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhdC1hY3Rpb24tZGlzcGF0Y2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjaGF0LWFjdGlvbi1kaXNwYXRjaC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsRUFBRTtBQUNGLDZGQUE2RjtBQUM3Riw0R0FBNEc7QUFDNUcsdUdBQXVHO0FBQ3ZHLG1HQUFtRztBQUNuRyx1R0FBdUc7QUFDdkcsNkdBQTZHO0FBQzdHLDZHQUE2RztBQUM3RywyR0FBMkc7QUFDM0csRUFBRTtBQUNGLHNHQUFzRztBQUN0RywwR0FBMEc7QUFDMUcscUNBQXFDO0FBRXJDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRy9ELDBFQUEwRTtBQUMxRSxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyw2QkFBNkIsQ0FBQztBQUN2RSw4R0FBOEc7QUFDOUcsTUFBTSxDQUFDLE1BQU0saUNBQWlDLEdBQUcsU0FBUyxDQUFDO0FBRTNEOzs7R0FHRztBQUNILE1BQU0sVUFBVSwyQkFBMkIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMvRixNQUFNLEdBQUcsR0FBRyxHQUFHLEVBQUUsQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQzdDLE9BQU8sT0FBTyxHQUFHLEtBQUssUUFBUSxJQUFJLEdBQUcsQ0FBQyxJQUFJLEVBQUUsS0FBSyxpQ0FBaUMsQ0FBQztBQUNyRixDQUFDO0FBU0Q7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGtCQUFrQixDQUN0QyxPQUEwQixFQUMxQixVQUdJLEVBQUU7SUFFTixNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUM7SUFFdkMsZ0ZBQWdGO0lBQ2hGLElBQUksQ0FBQywyQkFBMkIsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RDLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLFVBQVUsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO0lBQ3hFLENBQUM7SUFFRCxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsUUFBUSxJQUFJLGtCQUFrQixDQUFDO0lBQ3hELE1BQU0sTUFBTSxHQUFHLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNuQyxJQUFJLE1BQU0sS0FBSyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDN0MsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLGdCQUFnQixFQUFFLE1BQU0sRUFBRSxDQUFDO0lBQ3pELENBQUM7SUFFRCxNQUFNLFVBQVUsR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBRSxDQUFDO0lBQ3pDLElBQUksS0FBSyxDQUFDO0lBQ1YsSUFBSSxDQUFDO1FBQ0gsS0FBSyxHQUFHLFVBQVUsQ0FBQyxlQUFlLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxLQUFLLElBQUksQ0FBQztJQUMvRCxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLDRGQUE0RjtRQUM1RixPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztJQUN4SCxDQUFDO0lBQ0QsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLGdCQUFnQixFQUFFLE1BQU0sRUFBRSxDQUFDO0lBQ3pELENBQUM7SUFFRCxJQUFJLE1BQU0sQ0FBQztJQUNYLElBQUksQ0FBQztRQUNILE1BQU0sR0FBRyxNQUFNLFVBQVUsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDN0MsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLHVHQUF1RztRQUN2RyxpR0FBaUc7UUFDakcsdUdBQXVHO1FBQ3ZHLHdHQUF3RztRQUN4Ryx5R0FBeUc7UUFDekcsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLGVBQWUsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUN4RCxDQUFDO0lBQ0QsT0FBTyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLFlBQVksRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFDNUQsQ0FBQztBQUVELGlGQUFpRjtBQUNqRixTQUFTLFVBQVUsQ0FBQyxPQUEwQjtJQUM1QyxPQUFPLE9BQU8sSUFBSSxPQUFPLE9BQU8sQ0FBQyxNQUFNLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7QUFDL0UsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/chat-action-registry.d.ts b/packages/loopover-miner/lib/chat-action-registry.d.ts deleted file mode 100644 index d7b370ba9a..0000000000 --- a/packages/loopover-miner/lib/chat-action-registry.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type ChatActionRequest = { - action?: string; - params?: unknown; - governorInput?: unknown; -}; -/** A handler produced by {@link governorGatedHandler}; the only shape {@link ChatActionRegistry.register} accepts. */ -export type GovernorGatedHandler = (request: ChatActionRequest) => Promise>; -export type ChatActionDefinition = { - paramsValidator: (params: unknown) => boolean; - handler: GovernorGatedHandler; -}; -export type ChatActionEntry = { - paramsValidator: (params: unknown) => boolean; - handler: GovernorGatedHandler; -}; -export type ChatActionRegistry = { - register(name: string, definition: ChatActionDefinition): ChatActionEntry; - get(name: string): ChatActionEntry | undefined; - has(name: string): boolean; - names(): string[]; - readonly size: number; -}; -/** - * Wrap a local-write `run` function into a Governor-gated chat-action handler. The returned handler - * evaluates the write against the full precedence ladder (via `evaluateGovernorChokepointGate`) BEFORE - * running `run`, and only invokes `run` on a final `"allow"` verdict -- any other stage returns a gated - * result and `run` never executes. This is the ONLY factory that produces a handler `register` accepts. - */ -export declare function governorGatedHandler(run: (request: ChatActionRequest, gate: unknown) => unknown, options?: { - evaluateGate?: (input: unknown, gateOptions?: unknown) => unknown; - gateOptions?: unknown; -}): GovernorGatedHandler; -/** True only for a handler produced by {@link governorGatedHandler}. */ -export declare function isGovernorGatedHandler(handler: unknown): boolean; -/** - * Build an isolated chat-action registry. Child issues register into the shared {@link chatActionRegistry}; - * this factory exists so tests (and any future multi-registry consumer) can register without polluting it. - */ -export declare function createChatActionRegistry(): ChatActionRegistry; -/** The single shared registry the dispatch layer reads. Ships EMPTY (#6519); child issues register into it. */ -export declare const chatActionRegistry: ChatActionRegistry; -/** Register a chat action on the shared {@link chatActionRegistry}. */ -export declare function registerChatAction(name: string, definition: ChatActionDefinition): ChatActionEntry; diff --git a/packages/loopover-miner/lib/chat-action-registry.js b/packages/loopover-miner/lib/chat-action-registry.js deleted file mode 100644 index 88c0537e1e..0000000000 --- a/packages/loopover-miner/lib/chat-action-registry.js +++ /dev/null @@ -1,90 +0,0 @@ -// Allowlist registry + governor-gated handler contract for chat-issued miner actions (#6519). -// -// Shared scaffolding ONLY: this module ships with ZERO registered actions. The three action-family child -// issues (portfolio release/requeue, governor pause/resume, discover/attempt) register their handlers into -// this registry -- none are added here, and the default `chatActionRegistry` instance starts empty. -// -// The registration contract is the safety boundary. `register` refuses any handler that was not produced by -// `governorGatedHandler()`, and `governorGatedHandler()` routes every invocation through -// `evaluateGovernorChokepointGate` (packages/loopover-miner/lib/governor-chokepoint.js) and, through it, the -// fail-closed precedence ladder in packages/loopover-engine/src/governor/chokepoint.ts. Because a raw -// function can never be registered, a chat action can never perform a write on a path that bypasses the -// Governor chokepoint -- the contract enforces it structurally, not by review discipline. This module adds -// no second, competing safety check; it only forces every registered handler onto the existing one. -import { evaluateGovernorChokepointGate } from "./governor-chokepoint.js"; -// Private brand. Not exported, so external code cannot forge a "gated" marker onto a raw function: the only -// way to obtain a handler that passes `isGovernorGatedHandler` is to build it through `governorGatedHandler`. -const GOVERNOR_GATED = Symbol("loopover.chat-action.governor-gated"); -/** - * Wrap a local-write `run` function into a Governor-gated chat-action handler. The returned handler - * evaluates the write against the full precedence ladder (via `evaluateGovernorChokepointGate`) BEFORE - * running `run`, and only invokes `run` on a final `"allow"` verdict -- any other stage returns a gated - * result and `run` never executes. This is the ONLY factory that produces a handler `register` accepts. - */ -export function governorGatedHandler(run, options = {}) { - if (typeof run !== "function") { - throw new TypeError("governorGatedHandler(run): run must be a function"); - } - // Widen the default chokepoint evaluator to the registry's `unknown` input contract (chat requests carry - // opaque governorInput); runtime still passes the same value through unchanged. - const evaluateGate = options.evaluateGate ?? evaluateGovernorChokepointGate; - if (typeof evaluateGate !== "function") { - throw new TypeError("governorGatedHandler: options.evaluateGate must be a function when supplied"); - } - const handler = (async (request) => { - const gate = evaluateGate(request?.governorInput, options.gateOptions); - if (gate?.decision?.stage !== "allow") { - return { ok: false, status: "gated", decision: gate?.decision ?? null }; - } - const result = await run(request, gate); - return { ok: true, status: "executed", decision: gate.decision, result }; - }); - Object.defineProperty(handler, GOVERNOR_GATED, { value: true }); - return handler; -} -/** True only for a handler produced by {@link governorGatedHandler}. */ -export function isGovernorGatedHandler(handler) { - return typeof handler === "function" && handler[GOVERNOR_GATED] === true; -} -/** - * Build an isolated chat-action registry. Child issues register into the shared {@link chatActionRegistry}; - * this factory exists so tests (and any future multi-registry consumer) can register without polluting it. - */ -export function createChatActionRegistry() { - const actions = new Map(); - function register(name, definition = {}) { - if (typeof name !== "string" || name.trim() === "") { - throw new TypeError("registerChatAction(name): name must be a non-empty string"); - } - if (actions.has(name)) { - throw new Error(`registerChatAction: action "${name}" is already registered`); - } - const { paramsValidator, handler } = definition; - if (typeof paramsValidator !== "function") { - throw new TypeError(`registerChatAction("${name}"): paramsValidator must be a function`); - } - if (!isGovernorGatedHandler(handler)) { - throw new Error(`registerChatAction("${name}"): handler must be produced by governorGatedHandler() so every ` + - "chat-triggered write routes through governor-chokepoint.js -- a raw handler is rejected."); - } - const entry = { paramsValidator, handler }; - actions.set(name, entry); - return entry; - } - return { - register, - get: (name) => actions.get(name), - has: (name) => actions.has(name), - names: () => [...actions.keys()], - get size() { - return actions.size; - }, - }; -} -/** The single shared registry the dispatch layer reads. Ships EMPTY (#6519); child issues register into it. */ -export const chatActionRegistry = createChatActionRegistry(); -/** Register a chat action on the shared {@link chatActionRegistry}. */ -export function registerChatAction(name, definition) { - return chatActionRegistry.register(name, definition); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhdC1hY3Rpb24tcmVnaXN0cnkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjaGF0LWFjdGlvbi1yZWdpc3RyeS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw4RkFBOEY7QUFDOUYsRUFBRTtBQUNGLHlHQUF5RztBQUN6RywyR0FBMkc7QUFDM0csb0dBQW9HO0FBQ3BHLEVBQUU7QUFDRiw0R0FBNEc7QUFDNUcseUZBQXlGO0FBQ3pGLDZHQUE2RztBQUM3RyxzR0FBc0c7QUFDdEcsd0dBQXdHO0FBQ3hHLDJHQUEyRztBQUMzRyxvR0FBb0c7QUFFcEcsT0FBTyxFQUFFLDhCQUE4QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUE2QjFFLDRHQUE0RztBQUM1Ryw4R0FBOEc7QUFDOUcsTUFBTSxjQUFjLEdBQUcsTUFBTSxDQUFDLHFDQUFxQyxDQUFDLENBQUM7QUFRckU7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQ2xDLEdBQTJELEVBQzNELFVBR0ksRUFBRTtJQUVOLElBQUksT0FBTyxHQUFHLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDOUIsTUFBTSxJQUFJLFNBQVMsQ0FBQyxtREFBbUQsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFDRCx5R0FBeUc7SUFDekcsZ0ZBQWdGO0lBQ2hGLE1BQU0sWUFBWSxHQUNoQixPQUFPLENBQUMsWUFBWSxJQUFLLDhCQUFxRixDQUFDO0lBQ2pILElBQUksT0FBTyxZQUFZLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDdkMsTUFBTSxJQUFJLFNBQVMsQ0FBQyw2RUFBNkUsQ0FBQyxDQUFDO0lBQ3JHLENBQUM7SUFFRCxNQUFNLE9BQU8sR0FBRyxDQUFDLEtBQUssRUFBRSxPQUEwQixFQUFvQyxFQUFFO1FBQ3RGLE1BQU0sSUFBSSxHQUFHLFlBQVksQ0FBQyxPQUFPLEVBQUUsYUFBYSxFQUFFLE9BQU8sQ0FBQyxXQUFXLENBQXlCLENBQUM7UUFDL0YsSUFBSSxJQUFJLEVBQUUsUUFBUSxFQUFFLEtB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/chat-discover-attempt-actions.d.ts b/packages/loopover-miner/lib/chat-discover-attempt-actions.d.ts deleted file mode 100644 index b5226e13d9..0000000000 --- a/packages/loopover-miner/lib/chat-discover-attempt-actions.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ChatActionRegistry } from "./chat-action-registry.js"; -export declare const DISCOVER_CHAT_ACTION = "discover"; -export declare const ATTEMPT_CHAT_ACTION = "attempt"; -export type DiscoverChatActionInput = { - targets?: string[]; - search?: string; - dryRun?: boolean; - json?: boolean; - apiBaseUrl?: string; - tokenEnv?: string; -}; -export type AttemptChatActionInput = { - repoFullName: string; - issueNumber: number; - minerLogin: string; - base?: string; - live?: boolean; - dryRun?: boolean; - json?: boolean; -}; -/** - * `DiscoverActionInput` — every field optional (the CLI defaults them all), so an empty object is a valid - * "discover with defaults". Unknown keys are rejected rather than ignored: these params can be model-authored, - * and a typo'd flag must fail loudly instead of silently running a different discovery than intended. - */ -export declare function isDiscoverChatParams(params: unknown): boolean; -/** - * `AttemptActionInput` — `repoFullName` / `issueNumber` / `minerLogin` are REQUIRED (the CLI has no default - * for which issue to attempt), so unlike discover there is no valid empty form. `issueNumber` must be a - * positive integer: a float or 0 would reach the CLI as a nonsense issue reference. - */ -export declare function isAttemptChatParams(params: unknown): boolean; -/** Idempotently register `discover` / `attempt`. */ -export declare function registerDiscoverAttemptChatActions(options: { - requestDiscover: (input: DiscoverChatActionInput) => Promise; - requestAttempt: (input: AttemptChatActionInput) => Promise; - registry?: ChatActionRegistry; - evaluateGate?: () => { - decision: { - stage: string; - }; - }; -}): void; diff --git a/packages/loopover-miner/lib/chat-discover-attempt-actions.js b/packages/loopover-miner/lib/chat-discover-attempt-actions.js deleted file mode 100644 index b4a3961719..0000000000 --- a/packages/loopover-miner/lib/chat-discover-attempt-actions.js +++ /dev/null @@ -1,121 +0,0 @@ -// Discover/attempt chat-action registrations (#6837). -// -// The third and last child of the chat action-dispatch scaffolding (#6519) — chat-action-registry.js:4-5 -// names all three families (portfolio release/requeue, governor pause/resume, discover/attempt); the other -// two already ship. Registers `discover` / `attempt` into a chat-action registry. Handlers MUST be wired to -// the miner-ui clients `requestDiscover` / `requestAttempt` (apps/loopover-miner-ui/src/lib/{discover, -// attempt}.ts), so chat POSTs the SAME `/api/discover` and `/api/attempt` routes that already exist (#6522, -// registered at vite.config.ts:36-37) — never discover-cli.js/attempt-cli.js directly, and never a -// hand-rolled fetch. The miner-ui wire module passes those clients in; this module only owns the registration -// contract + params validators. -// -// GATING — the gate lives at the endpoint, not here, and that is deliberate: -// * `attempt` INHERITS the real Governor chokepoint for free: the route calls the real, unmodified -// `runAttempt`, and attempt-runner.js routes every write through -// `evaluateGovernorChokepointGatePersisted` before executing it (vite-attempt-api.ts:7-9). -// * `discover` has no chokepoint because it performs no gated write — it only fans out, ranks and enqueues -// (vite-discover-api.ts:13-14), so the CLI has none and the route adds none. -// Re-evaluating the chokepoint here would therefore be a SECOND, competing gate on a path that already has -// one (or needs none) — exactly what those route comments rule out, and it would gate chat more strictly than -// the equivalent CLI invocation. So, like chat-governor-actions.js and chat-portfolio-actions.js, we satisfy -// the registry's `governorGatedHandler` brand with an allow-stage evaluateGate. Execution still stays behind -// the shared LOOPOVER_MINER_CHAT_ACTIONS flag via `dispatchChatAction`, and `evaluateGate` stays injectable. -import { governorGatedHandler, chatActionRegistry } from "./chat-action-registry.js"; -export const DISCOVER_CHAT_ACTION = "discover"; -export const ATTEMPT_CHAT_ACTION = "attempt"; -/** The endpoint owns the gate (see the header note); satisfy the registry brand only. */ -const allowEndpointGatedAction = () => ({ decision: { stage: "allow" } }); -const DISCOVER_KEYS = new Set(["targets", "search", "dryRun", "json", "apiBaseUrl", "tokenEnv"]); -const ATTEMPT_KEYS = new Set(["repoFullName", "issueNumber", "minerLogin", "base", "live", "dryRun", "json"]); -function asParamsRecord(params) { - if (params == null || typeof params !== "object" || Array.isArray(params)) - return null; - return params; -} -/** A non-empty string — the shape every required text field here needs. */ -function isNonEmptyString(value) { - return typeof value === "string" && value.trim() !== ""; -} -/** - * `DiscoverActionInput` — every field optional (the CLI defaults them all), so an empty object is a valid - * "discover with defaults". Unknown keys are rejected rather than ignored: these params can be model-authored, - * and a typo'd flag must fail loudly instead of silently running a different discovery than intended. - */ -export function isDiscoverChatParams(params) { - if (params == null) - return true; - const record = asParamsRecord(params); - if (record === null) - return false; - for (const key of Object.keys(record)) { - if (!DISCOVER_KEYS.has(key)) - return false; - } - if (record.targets !== undefined) { - if (!Array.isArray(record.targets) || !record.targets.every(isNonEmptyString)) - return false; - } - for (const key of ["search", "apiBaseUrl", "tokenEnv"]) { - if (record[key] !== undefined && typeof record[key] !== "string") - return false; - } - for (const key of ["dryRun", "json"]) { - if (record[key] !== undefined && typeof record[key] !== "boolean") - return false; - } - return true; -} -/** - * `AttemptActionInput` — `repoFullName` / `issueNumber` / `minerLogin` are REQUIRED (the CLI has no default - * for which issue to attempt), so unlike discover there is no valid empty form. `issueNumber` must be a - * positive integer: a float or 0 would reach the CLI as a nonsense issue reference. - */ -export function isAttemptChatParams(params) { - const record = asParamsRecord(params); - if (record === null) - return false; - for (const key of Object.keys(record)) { - if (!ATTEMPT_KEYS.has(key)) - return false; - } - if (!isNonEmptyString(record.repoFullName)) - return false; - if (!isNonEmptyString(record.minerLogin)) - return false; - if (!Number.isInteger(record.issueNumber) || record.issueNumber <= 0) - return false; - if (record.base !== undefined && typeof record.base !== "string") - return false; - for (const key of ["live", "dryRun", "json"]) { - if (record[key] !== undefined && typeof record[key] !== "boolean") - return false; - } - return true; -} -/** Idempotently register `discover` / `attempt`. */ -export function registerDiscoverAttemptChatActions(options) { - const requestDiscover = options?.requestDiscover; - const requestAttempt = options?.requestAttempt; - if (typeof requestDiscover !== "function") { - throw new TypeError("registerDiscoverAttemptChatActions: requestDiscover must be a function"); - } - if (typeof requestAttempt !== "function") { - throw new TypeError("registerDiscoverAttemptChatActions: requestAttempt must be a function"); - } - const registry = options.registry ?? chatActionRegistry; - const evaluateGate = options.evaluateGate ?? allowEndpointGatedAction; - if (!registry.has(DISCOVER_CHAT_ACTION)) { - registry.register(DISCOVER_CHAT_ACTION, { - paramsValidator: isDiscoverChatParams, - // Nullish params mean "discover with defaults" -- forwarded as {} so the client always POSTs an object. - handler: governorGatedHandler(async (request) => requestDiscover((asParamsRecord(request?.params) ?? {})), { evaluateGate }), - }); - } - if (!registry.has(ATTEMPT_CHAT_ACTION)) { - registry.register(ATTEMPT_CHAT_ACTION, { - paramsValidator: isAttemptChatParams, - handler: governorGatedHandler(async (request) => requestAttempt(asParamsRecord(request?.params)), { evaluateGate }), - }); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhdC1kaXNjb3Zlci1hdHRlbXB0LWFjdGlvbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjaGF0LWRpc2NvdmVyLWF0dGVtcHQtYWN0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxzREFBc0Q7QUFDdEQsRUFBRTtBQUNGLHlHQUF5RztBQUN6RywyR0FBMkc7QUFDM0csNEdBQTRHO0FBQzVHLHVHQUF1RztBQUN2Ryw0R0FBNEc7QUFDNUcsbUdBQW1HO0FBQ25HLDhHQUE4RztBQUM5RyxnQ0FBZ0M7QUFDaEMsRUFBRTtBQUNGLDZFQUE2RTtBQUM3RSxxR0FBcUc7QUFDckcscUVBQXFFO0FBQ3JFLCtGQUErRjtBQUMvRiw2R0FBNkc7QUFDN0csaUZBQWlGO0FBQ2pGLDJHQUEyRztBQUMzRyw4R0FBOEc7QUFDOUcsNkdBQTZHO0FBQzdHLDZHQUE2RztBQUM3Ryw2R0FBNkc7QUFFN0csT0FBTyxFQUFFLG9CQUFvQixFQUFFLGtCQUFrQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFHckYsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsVUFBVSxDQUFDO0FBQy9DLE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLFNBQVMsQ0FBQztBQXFCN0MseUZBQXlGO0FBQ3pGLE1BQU0sd0JBQXdCLEdBQUcsR0FBRyxFQUFFLENBQUMsQ0FBQyxFQUFFLFFBQVEsRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFFMUUsTUFBTSxhQUFhLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxTQUFTLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsWUFBWSxFQUFFLFVBQVUsQ0FBQyxDQUFDLENBQUM7QUFDakcsTUFBTSxZQUFZLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxjQUFjLEVBQUUsYUFBYSxFQUFFLFlBQVksRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxNQUFNLENBQUMsQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/chat-governor-actions.d.ts b/packages/loopover-miner/lib/chat-governor-actions.d.ts deleted file mode 100644 index aef540a029..0000000000 --- a/packages/loopover-miner/lib/chat-governor-actions.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ChatActionRegistry } from "./chat-action-registry.js"; -export declare const GOVERNOR_PAUSE_CHAT_ACTION = "governor_pause"; -export declare const GOVERNOR_RESUME_CHAT_ACTION = "governor_resume"; -/** Optional `{ reason?: string }` — absent/empty params are valid; a non-string reason is rejected. */ -export declare function isGovernorPauseChatParams(params: unknown): boolean; -/** Resume takes no arguments — only nullish or an empty object is valid. */ -export declare function isGovernorResumeChatParams(params: unknown): boolean; -/** Idempotently register `governor_pause` / `governor_resume`. */ -export declare function registerGovernorChatActions(options: { - pauseGovernor: (reason?: string) => Promise; - resumeGovernor: () => Promise; - registry?: ChatActionRegistry; - evaluateGate?: () => { - decision: { - stage: string; - }; - }; -}): void; diff --git a/packages/loopover-miner/lib/chat-governor-actions.js b/packages/loopover-miner/lib/chat-governor-actions.js deleted file mode 100644 index 733f92524c..0000000000 --- a/packages/loopover-miner/lib/chat-governor-actions.js +++ /dev/null @@ -1,75 +0,0 @@ -// Governor pause/resume chat-action registrations (#6521). -// -// Child issue of the chat action-dispatch scaffolding (#6519). Registers `governor_pause` / -// `governor_resume` into a chat-action registry. Handlers MUST be wired to the miner-ui clients -// `pauseGovernor` / `resumeGovernor` (apps/loopover-miner-ui/src/lib/governor.ts) — never to -// governor-state.js and never via a hand-rolled fetch. The miner-ui wire module passes those clients -// in; this module only owns the registration contract + params validators. -// -// Pause/resume is administrative control, not a chokepoint content-write. The registry still requires -// a `governorGatedHandler` brand, so we supply an allow-stage evaluateGate rather than routing through -// governor-chokepoint.js. Execution stays behind the shared LOOPOVER_MINER_CHAT_ACTIONS flag via -// `dispatchChatAction`. -import { governorGatedHandler, chatActionRegistry } from "./chat-action-registry.js"; -export const GOVERNOR_PAUSE_CHAT_ACTION = "governor_pause"; -export const GOVERNOR_RESUME_CHAT_ACTION = "governor_resume"; -/** Administrative pause/resume is not a chokepoint content-write (#6521); satisfy the registry brand only. */ -const allowAdministrativeGate = () => ({ decision: { stage: "allow" } }); -/** Optional `{ reason?: string }` — absent/empty params are valid; a non-string reason is rejected. */ -export function isGovernorPauseChatParams(params) { - if (params == null) - return true; - if (typeof params !== "object" || Array.isArray(params)) - return false; - const keys = Object.keys(params); - if (keys.length === 0) - return true; - if (keys.length === 1 && keys[0] === "reason") { - const reason = params.reason; - return reason === undefined || typeof reason === "string"; - } - return false; -} -/** Resume takes no arguments — only nullish or an empty object is valid. */ -export function isGovernorResumeChatParams(params) { - if (params == null) - return true; - if (typeof params !== "object" || Array.isArray(params)) - return false; - return Object.keys(params).length === 0; -} -function readOptionalPauseReason(params) { - if (params == null || typeof params !== "object" || Array.isArray(params)) - return undefined; - const reason = params.reason; - // Mirror LedgersPage: empty string → undefined so pauseGovernor omits the body field. - return typeof reason === "string" && reason ? reason : undefined; -} -/** Idempotently register `governor_pause` / `governor_resume`. */ -export function registerGovernorChatActions(options) { - const pauseGovernor = options?.pauseGovernor; - const resumeGovernor = options?.resumeGovernor; - if (typeof pauseGovernor !== "function") { - throw new TypeError("registerGovernorChatActions: pauseGovernor must be a function"); - } - if (typeof resumeGovernor !== "function") { - throw new TypeError("registerGovernorChatActions: resumeGovernor must be a function"); - } - const registry = options.registry ?? chatActionRegistry; - const evaluateGate = options.evaluateGate ?? allowAdministrativeGate; - if (!registry.has(GOVERNOR_PAUSE_CHAT_ACTION)) { - registry.register(GOVERNOR_PAUSE_CHAT_ACTION, { - paramsValidator: isGovernorPauseChatParams, - handler: governorGatedHandler(async (request) => pauseGovernor(readOptionalPauseReason(request?.params)), { - evaluateGate, - }), - }); - } - if (!registry.has(GOVERNOR_RESUME_CHAT_ACTION)) { - registry.register(GOVERNOR_RESUME_CHAT_ACTION, { - paramsValidator: isGovernorResumeChatParams, - handler: governorGatedHandler(async () => resumeGovernor(), { evaluateGate }), - }); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhdC1nb3Zlcm5vci1hY3Rpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2hhdC1nb3Zlcm5vci1hY3Rpb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDJEQUEyRDtBQUMzRCxFQUFFO0FBQ0YsNEZBQTRGO0FBQzVGLGdHQUFnRztBQUNoRyw2RkFBNkY7QUFDN0YscUdBQXFHO0FBQ3JHLDJFQUEyRTtBQUMzRSxFQUFFO0FBQ0Ysc0dBQXNHO0FBQ3RHLHVHQUF1RztBQUN2RyxpR0FBaUc7QUFDakcsd0JBQXdCO0FBRXhCLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBR3JGLE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLGdCQUFnQixDQUFDO0FBQzNELE1BQU0sQ0FBQyxNQUFNLDJCQUEyQixHQUFHLGlCQUFpQixDQUFDO0FBRTdELDhHQUE4RztBQUM5RyxNQUFNLHVCQUF1QixHQUFHLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxRQUFRLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBRXpFLHVHQUF1RztBQUN2RyxNQUFNLFVBQVUseUJBQXlCLENBQUMsTUFBZTtJQUN2RCxJQUFJLE1BQU0sSUFBSSxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDaEMsSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUN0RSxNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQWdCLENBQUMsQ0FBQztJQUMzQyxJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ25DLElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxDQUFDLElBQUksSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLFFBQVEsRUFBRSxD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/chat-portfolio-actions.d.ts b/packages/loopover-miner/lib/chat-portfolio-actions.d.ts deleted file mode 100644 index 6da12f0583..0000000000 --- a/packages/loopover-miner/lib/chat-portfolio-actions.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ChatActionRegistry } from "./chat-action-registry.js"; -export declare const PORTFOLIO_RELEASE_CHAT_ACTION = "portfolio_release"; -export declare const PORTFOLIO_REQUEUE_CHAT_ACTION = "portfolio_requeue"; -export type PortfolioChatActionItem = { - repoFullName: string; - identifier: string; - apiBaseUrl?: string; -}; -/** - * Params for both actions: the queue item to act on. `repoFullName` + `identifier` are required non-empty - * strings; `apiBaseUrl` is optional (the route defaults it, mirroring the client's own - * `Pick` shape, where the buttons - * always pass one but the CLI path does not). Unknown keys are rejected rather than ignored: a typo'd param - * from a model-authored call must fail loudly, not silently act on the wrong item. - */ -export declare function isPortfolioItemChatParams(params: unknown): boolean; -/** Idempotently register `portfolio_release` / `portfolio_requeue`. */ -export declare function registerPortfolioChatActions(options: { - releaseItem: (item: PortfolioChatActionItem) => Promise; - requeueItem: (item: PortfolioChatActionItem) => Promise; - registry?: ChatActionRegistry; - evaluateGate?: () => { - decision: { - stage: string; - }; - }; -}): void; diff --git a/packages/loopover-miner/lib/chat-portfolio-actions.js b/packages/loopover-miner/lib/chat-portfolio-actions.js deleted file mode 100644 index 867b160e40..0000000000 --- a/packages/loopover-miner/lib/chat-portfolio-actions.js +++ /dev/null @@ -1,85 +0,0 @@ -// Portfolio release/requeue chat-action registrations (#6838). -// -// Child issue of the chat action-dispatch scaffolding (#6519). Registers `portfolio_release` / -// `portfolio_requeue` into a chat-action registry. Handlers MUST be wired to the miner-ui clients -// `releasePortfolioQueueItem` / `requeuePortfolioQueueItem` (apps/loopover-miner-ui/src/lib/ -// portfolio-queue-actions.ts), so chat POSTs the SAME `/api/portfolio-queue/{release,requeue}` routes the -// dashboard's existing buttons already call — never portfolio-queue.js directly, and never a hand-rolled -// fetch. The miner-ui wire module passes those clients in; this module only owns the registration contract -// + params validators. That is what keeps chat from becoming a parallel write path (#6504's design). -// -// Release/requeue is local queue administration, not a chokepoint content-write: the route it lands on is a -// thin bridge to the same store methods the CLI's `queue release` / `queue requeue` already use -// (vite-portfolio-queue-actions-api.ts → reclaimStuckItem / requeueItem), and it invokes no chokepoint of its -// own. Requiring one only for the chat path would gate chat MORE strictly than the button beside it, which -// #6838 forbids ("No changes to the existing route or button-triggered flow"). So, exactly like -// chat-governor-actions.js's administrative pause/resume, we satisfy the registry's `governorGatedHandler` -// brand with an allow-stage evaluateGate rather than routing through governor-chokepoint.js. Execution still -// stays behind the shared LOOPOVER_MINER_CHAT_ACTIONS flag via `dispatchChatAction`. -import { governorGatedHandler, chatActionRegistry } from "./chat-action-registry.js"; -export const PORTFOLIO_RELEASE_CHAT_ACTION = "portfolio_release"; -export const PORTFOLIO_REQUEUE_CHAT_ACTION = "portfolio_requeue"; -/** Local queue administration is not a chokepoint content-write (#6838); satisfy the registry brand only. */ -const allowAdministrativeGate = () => ({ decision: { stage: "allow" } }); -/** - * Params for both actions: the queue item to act on. `repoFullName` + `identifier` are required non-empty - * strings; `apiBaseUrl` is optional (the route defaults it, mirroring the client's own - * `Pick` shape, where the buttons - * always pass one but the CLI path does not). Unknown keys are rejected rather than ignored: a typo'd param - * from a model-authored call must fail loudly, not silently act on the wrong item. - */ -export function isPortfolioItemChatParams(params) { - if (params == null || typeof params !== "object" || Array.isArray(params)) - return false; - const record = params; - for (const key of Object.keys(record)) { - if (key !== "repoFullName" && key !== "identifier" && key !== "apiBaseUrl") - return false; - } - if (typeof record.repoFullName !== "string" || record.repoFullName.trim() === "") - return false; - if (typeof record.identifier !== "string" || record.identifier.trim() === "") - return false; - if (record.apiBaseUrl !== undefined && typeof record.apiBaseUrl !== "string") - return false; - return true; -} -/** - * Narrow validated params to the client's item shape. `apiBaseUrl` is only forwarded when present, so an - * omitted one stays omitted rather than becoming an explicit `undefined` in the POST body. - */ -function readPortfolioItem(params) { - const record = params; - const item = { repoFullName: record.repoFullName, identifier: record.identifier }; - return typeof record.apiBaseUrl === "string" ? { ...item, apiBaseUrl: record.apiBaseUrl } : item; -} -/** Idempotently register `portfolio_release` / `portfolio_requeue`. */ -export function registerPortfolioChatActions(options) { - const releaseItem = options?.releaseItem; - const requeueItem = options?.requeueItem; - if (typeof releaseItem !== "function") { - throw new TypeError("registerPortfolioChatActions: releaseItem must be a function"); - } - if (typeof requeueItem !== "function") { - throw new TypeError("registerPortfolioChatActions: requeueItem must be a function"); - } - const registry = options.registry ?? chatActionRegistry; - const evaluateGate = options.evaluateGate ?? allowAdministrativeGate; - if (!registry.has(PORTFOLIO_RELEASE_CHAT_ACTION)) { - registry.register(PORTFOLIO_RELEASE_CHAT_ACTION, { - paramsValidator: isPortfolioItemChatParams, - handler: governorGatedHandler(async (request) => releaseItem(readPortfolioItem(request?.params)), { - evaluateGate, - }), - }); - } - if (!registry.has(PORTFOLIO_REQUEUE_CHAT_ACTION)) { - registry.register(PORTFOLIO_REQUEUE_CHAT_ACTION, { - paramsValidator: isPortfolioItemChatParams, - handler: governorGatedHandler(async (request) => requeueItem(readPortfolioItem(request?.params)), { - evaluateGate, - }), - }); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhdC1wb3J0Zm9saW8tYWN0aW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNoYXQtcG9ydGZvbGlvLWFjdGlvbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0RBQStEO0FBQy9ELEVBQUU7QUFDRiwrRkFBK0Y7QUFDL0Ysa0dBQWtHO0FBQ2xHLDZGQUE2RjtBQUM3RiwwR0FBMEc7QUFDMUcseUdBQXlHO0FBQ3pHLDJHQUEyRztBQUMzRyxxR0FBcUc7QUFDckcsRUFBRTtBQUNGLDRHQUE0RztBQUM1RyxnR0FBZ0c7QUFDaEcsOEdBQThHO0FBQzlHLDJHQUEyRztBQUMzRyxnR0FBZ0c7QUFDaEcsMkdBQTJHO0FBQzNHLDZHQUE2RztBQUM3RyxxRkFBcUY7QUFFckYsT0FBTyxFQUFFLG9CQUFvQixFQUFFLGtCQUFrQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFHckYsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsbUJBQW1CLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsbUJBQW1CLENBQUM7QUFRakUsNkdBQTZHO0FBQzdHLE1BQU0sdUJBQXVCLEdBQUcsR0FBRyxFQUFFLENBQUMsQ0FBQyxFQUFFLFFBQVEsRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFFekU7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLHlCQUF5QixDQUFDLE1BQWU7SUFDdkQsSUFBSSxNQUFNLElBQUksSUFBSSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQztRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3hGLE1BQU0sTUFBTSxHQUFHLE1BQWlDLENBQUM7SUFDakQsS0FBSyxNQUFNLEdBQUcsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDdEMsSUFBSSxHQUFHLEtBQUssY0FBYyxJQUFJLEdBQUcs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/ci-poller.d.ts b/packages/loopover-miner/lib/ci-poller.d.ts deleted file mode 100644 index fe1d647282..0000000000 --- a/packages/loopover-miner/lib/ci-poller.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type CheckRunConclusion = "pending" | "success" | "failure" | "neutral"; -export type NormalizedCheckRun = { - name: string; - status: string; - conclusion: CheckRunConclusion; - detailsUrl: string | null; - startedAt: string | null; - completedAt: string | null; -}; -export type PollCheckRunsResult = { - conclusion: CheckRunConclusion; - checks: NormalizedCheckRun[]; - headSha: string; - attempts: number; -}; -export type PollCheckRunsOptions = { - apiBaseUrl?: string; - fetchFn?: typeof fetch; - githubToken?: string; - maxAttempts?: number; - minIntervalMs?: number; - maxIntervalMs?: number; - requestTimeoutMs?: number; - sleepFn?: (delayMs: number) => Promise; -}; -export declare function pollCheckRuns(repoFullName: string, prNumber: number, options?: PollCheckRunsOptions): Promise; diff --git a/packages/loopover-miner/lib/ci-poller.js b/packages/loopover-miner/lib/ci-poller.js deleted file mode 100644 index e7804f1774..0000000000 --- a/packages/loopover-miner/lib/ci-poller.js +++ /dev/null @@ -1,215 +0,0 @@ -import { fetchWithRetry } from "./http-retry.js"; -const defaultApiBaseUrl = "https://api.github.com"; -const defaultMinIntervalMs = 60_000; -const defaultMaxIntervalMs = 5 * 60_000; -const defaultMaxAttempts = 1; -const defaultRequestTimeoutMs = 10_000; -const githubApiVersion = "2022-11-28"; -function normalizeApiBaseUrl(value) { - if (value === undefined) - return defaultApiBaseUrl; - if (typeof value !== "string" || !value.trim()) - return defaultApiBaseUrl; - let parsed; - try { - parsed = new URL(value.trim()); - } - catch { - throw new Error("invalid_api_base_url"); - } - if (parsed.protocol !== "https:" || parsed.hostname !== "api.github.com") { - throw new Error("invalid_api_base_url"); - } - parsed.pathname = parsed.pathname.replace(/\/+$/, ""); - parsed.search = ""; - parsed.hash = ""; - return parsed.toString().replace(/\/+$/, ""); -} -function normalizePositiveInt(value, fallback, min, max) { - if (!Number.isFinite(value)) - return fallback; - return Math.min(max, Math.max(min, Math.floor(value))); -} -function normalizeOptions(options = {}) { - return { - apiBaseUrl: normalizeApiBaseUrl(options.apiBaseUrl), - fetchFn: options.fetchFn ?? fetch, - githubToken: typeof options.githubToken === "string" ? options.githubToken.trim() : "", - maxAttempts: normalizePositiveInt(options.maxAttempts, defaultMaxAttempts, 1, 20), - minIntervalMs: normalizePositiveInt(options.minIntervalMs, defaultMinIntervalMs, 1, 60 * 60_000), - maxIntervalMs: normalizePositiveInt(options.maxIntervalMs, defaultMaxIntervalMs, 1, 60 * 60_000), - requestTimeoutMs: normalizePositiveInt(options.requestTimeoutMs, defaultRequestTimeoutMs, 1, 60_000), - sleepFn: options.sleepFn ?? - ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))), - }; -} -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner?.trim() || !repo?.trim() || extra !== undefined) { - throw new Error("invalid_repo_full_name"); - } - return { owner: owner.trim(), repo: repo.trim() }; -} -function normalizePullNumber(value) { - if (!Number.isInteger(value) || value <= 0) - throw new Error("invalid_pr_number"); - return value; -} -function githubHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "user-agent": "loopover-miner", - "x-github-api-version": githubApiVersion, - }; - if (githubToken) - headers.authorization = `Bearer ${githubToken}`; - return headers; -} -function repoPath(target, suffix) { - return `/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}${suffix}`; -} -function apiUrl(apiBaseUrl, path, query = "") { - return `${apiBaseUrl}${path}${query}`; -} -function githubError(response, payload) { - const code = `github_${response.status}`; - const record = payload; - const githubMessage = typeof record?.message === "string" && record.message.trim() ? record.message : null; - const message = githubMessage ? `${code}: ${githubMessage}` : code; - return Object.assign(new Error(message), { code, githubMessage }); -} -async function githubGetJsonResponse(url, options) { - // Retry transient network errors / 5xx around this single call (#4829), distinct from the poller's own - // pending-retry loop; the poller's injected sleepFn keeps tests instant. requestTimeoutMs bounds each - // individual attempt (a stalled connection previously hung this call forever -- #miner-github-read-timeouts). - const response = (await fetchWithRetry(options.fetchFn, url, { method: "GET", headers: githubHeaders(options.githubToken) }, { sleepFn: options.sleepFn, timeoutMs: options.requestTimeoutMs })); - const payload = await response.json().catch(() => null); - if (!response.ok) { - throw githubError(response, payload); - } - return { payload, response }; -} -async function githubGetJson(url, options) { - const { payload } = await githubGetJsonResponse(url, options); - return payload; -} -function hasNextLink(response) { - return /<[^>]+>;\s*rel="next"/.test(response.headers.get("link") ?? ""); -} -function payloadTotalCount(payload) { - const totalCount = Number(payload?.total_count); - return Number.isInteger(totalCount) && totalCount >= 0 ? totalCount : null; -} -function normalizeConclusion(checkRun) { - if (!checkRun || typeof checkRun !== "object") - return "pending"; - const run = checkRun; - if (run.status !== "completed") - return "pending"; - switch (run.conclusion) { - case "success": - case "skipped": - return "success"; - case "neutral": - return "neutral"; - case "failure": - case "cancelled": - case "timed_out": - case "action_required": - case "stale": - case "startup_failure": - return "failure"; - default: - return "pending"; - } -} -function normalizeCheckRun(checkRun) { - const run = checkRun; - return { - name: typeof run?.name === "string" ? run.name : "", - status: typeof run?.status === "string" ? run.status : "unknown", - conclusion: normalizeConclusion(checkRun), - detailsUrl: typeof run?.details_url === "string" ? run.details_url : null, - startedAt: typeof run?.started_at === "string" ? run.started_at : null, - completedAt: typeof run?.completed_at === "string" ? run.completed_at : null, - }; -} -function aggregateConclusion(checks) { - if (checks.length === 0) - return "pending"; - if (checks.some((check) => check.conclusion === "failure")) - return "failure"; - if (checks.some((check) => check.conclusion === "pending")) - return "pending"; - if (checks.every((check) => check.conclusion === "success")) - return "success"; - return "neutral"; -} -function backoffDelayMs(attemptIndex, options) { - const exponent = Math.min(10, Math.max(0, attemptIndex)); - return Math.min(options.maxIntervalMs, options.minIntervalMs * 2 ** exponent); -} -async function fetchHeadSha(target, prNumber, options) { - const payload = (await githubGetJson(apiUrl(options.apiBaseUrl, repoPath(target, `/pulls/${prNumber}`)), options)); - const headSha = payload?.head?.sha; - if (typeof headSha !== "string" || !headSha) - throw new Error("github_pr_head_sha_missing"); - return headSha; -} -async function fetchCheckRuns(target, headSha, options) { - const checks = []; - let page = 1; - let expectedTotalCount = null; - while (true) { - const { payload, response } = await githubGetJsonResponse(apiUrl(options.apiBaseUrl, repoPath(target, `/commits/${encodeURIComponent(headSha)}/check-runs`), `?per_page=100&page=${page}`), options); - const body = payload; - if (!Array.isArray(body?.check_runs)) { - throw new Error("github_check_runs_malformed"); - } - const pageChecks = body.check_runs.map(normalizeCheckRun); - checks.push(...pageChecks); - expectedTotalCount = payloadTotalCount(payload) ?? expectedTotalCount; - if (!hasNextLink(response) && (expectedTotalCount === null || checks.length >= expectedTotalCount)) { - return checks; - } - if (pageChecks.length === 0) { - throw new Error("github_check_runs_pagination_incomplete"); - } - page += 1; - } -} -export async function pollCheckRuns(repoFullName, prNumber, options = {}) { - const target = parseRepoFullName(repoFullName); - const normalizedPrNumber = normalizePullNumber(prNumber); - const normalizedOptions = normalizeOptions(options); - let latest = { conclusion: "pending", checks: [], headSha: "", attempts: 0 }; - for (let attempt = 0; attempt < normalizedOptions.maxAttempts; attempt += 1) { - const headSha = await fetchHeadSha(target, normalizedPrNumber, normalizedOptions); - const checks = await fetchCheckRuns(target, headSha, normalizedOptions); - latest = { - conclusion: aggregateConclusion(checks), - checks, - headSha, - attempts: attempt + 1, - }; - if (latest.conclusion !== "pending") { - const currentHeadSha = await fetchHeadSha(target, normalizedPrNumber, normalizedOptions); - if (currentHeadSha === headSha) { - return latest; - } - latest = { - conclusion: "pending", - checks: [], - headSha: currentHeadSha, - attempts: attempt + 1, - }; - } - if (attempt < normalizedOptions.maxAttempts - 1) { - await normalizedOptions.sleepFn(backoffDelayMs(attempt, normalizedOptions)); - } - } - return latest; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2ktcG9sbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2ktcG9sbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUVqRCxNQUFNLGlCQUFpQixHQUFHLHdCQUF3QixDQUFDO0FBQ25ELE1BQU0sb0JBQW9CLEdBQUcsTUFBTSxDQUFDO0FBQ3BDLE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQztBQUN4QyxNQUFNLGtCQUFrQixHQUFHLENBQUMsQ0FBQztBQUM3QixNQUFNLHVCQUF1QixHQUFHLE1BQU0sQ0FBQztBQUN2QyxNQUFNLGdCQUFnQixHQUFHLFlBQVksQ0FBQztBQTRDdEMsU0FBUyxtQkFBbUIsQ0FBQyxLQUFjO0lBQ3pDLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxPQUFPLGlCQUFpQixDQUFDO0lBQ2xELElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRTtRQUFFLE9BQU8saUJBQWlCLENBQUM7SUFDekUsSUFBSSxNQUFXLENBQUM7SUFDaEIsSUFBSSxDQUFDO1FBQ0gsTUFBTSxHQUFHLElBQUksR0FBRyxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ2pDLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxNQUFNLElBQUksS0FBSyxDQUFDLHNCQUFzQixDQUFDLENBQUM7SUFDMUMsQ0FBQztJQUNELElBQUksTUFBTSxDQUFDLFFBQVEsS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLFFBQVEsS0FBSyxnQkFBZ0IsRUFBRSxDQUFDO1FBQ3pFLE1BQU0sSUFBSSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMxQyxDQUFDO0lBQ0QsTUFBTSxDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-adjudication.d.ts b/packages/loopover-miner/lib/claim-adjudication.d.ts deleted file mode 100644 index f612e501e4..0000000000 --- a/packages/loopover-miner/lib/claim-adjudication.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { DuplicateClaimMember } from "@loopover/engine"; -/** An observed claim on an issue: a PR/claimant number plus when it claimed the linked issue (if known). */ -export type ObservedClaim = { - number: number; - claimedAt?: string | null | undefined; -}; -/** The engine `DuplicateClaimMember` shape this module bridges an {@link ObservedClaim} to. */ -export type ClaimMember = DuplicateClaimMember; -/** The adjudication result: the go/no-go `isWinner`, plus a DISPLAY-only `winnerNumber` (null when not determinable). */ -export type ClaimAdjudication = { - isWinner: boolean; - winnerNumber: number | null; -}; -/** - * Map an observed claim record to the engine's `DuplicateClaimMember`. The field names deliberately DIFFER — the - * local ledger / observed data expose `claimedAt`, the engine election reads `linkedIssueClaimedAt` — so the bridge - * is explicit (they are not interchangeable by accident of naming). `createdAt` is intentionally omitted: the - * election ignores it (an older PR can claim a linked issue later by editing its body). Pure. - */ -export declare function toClaimMember(claim: ObservedClaim): ClaimMember; -/** - * Adjudicate whether THIS miner's soft-claim wins a contested issue. `self` is this miner's claim and `competing` - * is the publicly-observable set of OTHER open PRs linking the same issue; each entry is `{ number, claimedAt }`. - * Returns the go/no-go `isWinner` (driven ONLY by `isDuplicateClusterWinnerByClaim`) plus a DISPLAY-only - * `winnerNumber` (from `resolveDuplicateClusterWinnerNumber`, for surfacing "you lost this claim to PR #N" to the - * operator — never for the decision). Pure — no IO. Fail-closed: a missing/sparse claim time loses; the winner is - * `null` when the ordering is too sparse to be sure (it never guesses). An empty `competing` list ⇒ trivial winner. - */ -export declare function adjudicateSoftClaim(self: ObservedClaim, competing?: readonly ObservedClaim[]): ClaimAdjudication; diff --git a/packages/loopover-miner/lib/claim-adjudication.js b/packages/loopover-miner/lib/claim-adjudication.js deleted file mode 100644 index 42b85246a2..0000000000 --- a/packages/loopover-miner/lib/claim-adjudication.js +++ /dev/null @@ -1,35 +0,0 @@ -// Soft-claim adjudication (#4291). Decides which of several miners claiming the same issue proceeds, by REUSING the -// maintainer-side duplicate-cluster election (`isDuplicateClusterWinnerByClaim` from @loopover/engine) -// rather than reimplementing it — so the miner and the maintainer gate agree on exactly one winner by construction. -// -// The local claim ledger is 100% client-side and cannot see other miners' claims, so the competing-claim signal -// must come from something publicly observable: the OPEN PRs that link the same issue (an issue with several open -// PRs linking it IS the public signal of a contested claim). The caller assembles that set — exactly like the -// maintainer-side callers in src/ do — and passes it here. -import { isDuplicateClusterWinnerByClaim, resolveDuplicateClusterWinnerNumber } from "@loopover/engine"; -/** - * Map an observed claim record to the engine's `DuplicateClaimMember`. The field names deliberately DIFFER — the - * local ledger / observed data expose `claimedAt`, the engine election reads `linkedIssueClaimedAt` — so the bridge - * is explicit (they are not interchangeable by accident of naming). `createdAt` is intentionally omitted: the - * election ignores it (an older PR can claim a linked issue later by editing its body). Pure. - */ -export function toClaimMember(claim) { - return { number: claim.number, linkedIssueClaimedAt: claim.claimedAt ?? null }; -} -/** - * Adjudicate whether THIS miner's soft-claim wins a contested issue. `self` is this miner's claim and `competing` - * is the publicly-observable set of OTHER open PRs linking the same issue; each entry is `{ number, claimedAt }`. - * Returns the go/no-go `isWinner` (driven ONLY by `isDuplicateClusterWinnerByClaim`) plus a DISPLAY-only - * `winnerNumber` (from `resolveDuplicateClusterWinnerNumber`, for surfacing "you lost this claim to PR #N" to the - * operator — never for the decision). Pure — no IO. Fail-closed: a missing/sparse claim time loses; the winner is - * `null` when the ordering is too sparse to be sure (it never guesses). An empty `competing` list ⇒ trivial winner. - */ -export function adjudicateSoftClaim(self, competing = []) { - const selfMember = toClaimMember(self); - const siblings = competing.map(toClaimMember); - return { - isWinner: isDuplicateClusterWinnerByClaim(selfMember, siblings), - winnerNumber: resolveDuplicateClusterWinnerNumber(selfMember, siblings), - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tYWRqdWRpY2F0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2xhaW0tYWRqdWRpY2F0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG9IQUFvSDtBQUNwSCx1R0FBdUc7QUFDdkcsb0hBQW9IO0FBQ3BILEVBQUU7QUFDRixnSEFBZ0g7QUFDaEgsa0hBQWtIO0FBQ2xILDhHQUE4RztBQUM5RywyREFBMkQ7QUFDM0QsT0FBTyxFQUFFLCtCQUErQixFQUFFLG1DQUFtQyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFrQnhHOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGFBQWEsQ0FBQyxLQUFvQjtJQUNoRCxPQUFPLEVBQUUsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNLEVBQUUsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVMsSUFBSSxJQUFJLEVBQUUsQ0FBQztBQUNqRixDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxJQUFtQixFQUFFLFlBQXNDLEVBQUU7SUFDL0YsTUFBTSxVQUFVLEdBQUcsYUFBYSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3ZDLE1BQU0sUUFBUSxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7SUFDOUMsT0FBTztRQUNMLFFBQVEsRUFBRSwrQkFBK0IsQ0FBQyxVQUFVLEVBQUUsUUFBUSxDQUFDO1FBQy9ELFlBQVksRUFBRSxtQ0FBbUMsQ0FBQyxVQUFVLEVBQUUsUUFBUSxDQUFDO0tBQ3hFLENBQUM7QUFDSixDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-conflict-resolver.d.ts b/packages/loopover-miner/lib/claim-conflict-resolver.d.ts deleted file mode 100644 index 023353d338..0000000000 --- a/packages/loopover-miner/lib/claim-conflict-resolver.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { LiveIssueSnapshot } from "./submission-freshness-check.js"; -import type { ObservedClaim } from "./claim-adjudication.js"; -import type { LocalWriteActionSpec } from "@loopover/engine"; -/** - * Assemble the real competing-claims set from a fetched LiveIssueSnapshot: every OTHER open PR referencing - * the issue, excluding `selfPrNumber` and any PR authored by `minerLogin` itself (case-insensitive, mirrors - * checkSubmissionFreshness's own author comparison -- a login can be echoed back with different casing). - * Excluding same-author PRs is deliberate, not an edge case slipping through: a miner never competes against - * its own work, so if this login somehow has ANOTHER open PR on the same issue (e.g. a retry after a crash - * left a stale one behind), that PR is never treated as a competing claim to lose against -- only a genuinely - * different claimant's PR can trigger a real close. - * Pure given its inputs. - * - * @param {import("./submission-freshness-check.js").LiveIssueSnapshot | null | undefined} snapshot - * @param {number} selfPrNumber - * @param {string} minerLogin - * @returns {import("./claim-adjudication.js").ObservedClaim[]} - */ -export declare function assembleCompetingClaims(snapshot: LiveIssueSnapshot | null | undefined, selfPrNumber: number, minerLogin: string): ObservedClaim[]; -export type ClaimConflictInput = { - repoFullName: string; - issueNumber: number; - selfPrNumber: number; - selfClaimedAt: string | null; - minerLogin: string; -}; -export type ClaimConflictDeps = { - fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise; - executeLocalWrite: (spec: LocalWriteActionSpec) => Promise; -}; -export type ClaimConflictResult = { - checked: false; - reason: "live_state_unavailable"; -} | { - checked: true; - isWinner: true; - winnerNumber: number | null; - competingCount: number; -} | { - checked: true; - isWinner: false; - winnerNumber: number | null; - competingCount: number; - closeResult: unknown; -}; -export type ClaimConflictRetryOptions = { - maxAttempts?: number; - sleepFn?: (ms: number) => Promise; - backoffMs?: (attempt: number) => number; -}; -/** - * Resolve a real claim conflict for an already-submitted PR. Fails OPEN (never closes anything) when the live - * snapshot can't be fetched -- an unavailable check is not evidence of a lost claim. - * - * @param {{ repoFullName: string, issueNumber: number, selfPrNumber: number, selfClaimedAt: string | null, minerLogin: string }} input - * @param {{ - * fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise, - * executeLocalWrite: (spec: import("@loopover/engine").LocalWriteActionSpec) => Promise, - * }} deps - * @param {{ maxAttempts?: number, sleepFn?: (ms: number) => Promise, backoffMs?: (attempt: number) => number }} [options] - * Bounded retry for the live-state snapshot fetch (#6058): up to `maxAttempts` (default 3) attempts with - * `backoffMs(attempt)` backoff between them, returning as soon as a competing claim is observed. Pure over - * the injected `sleepFn`/`backoffMs` -- no real timers in tests. - * @returns {Promise<{ - * checked: boolean, - * reason?: "live_state_unavailable", - * isWinner?: boolean, - * winnerNumber?: number | null, - * competingCount?: number, - * closeResult?: unknown, - * }>} - */ -export declare function resolveClaimConflict(input: ClaimConflictInput, deps: ClaimConflictDeps, options?: ClaimConflictRetryOptions): Promise; diff --git a/packages/loopover-miner/lib/claim-conflict-resolver.js b/packages/loopover-miner/lib/claim-conflict-resolver.js deleted file mode 100644 index 2492133954..0000000000 --- a/packages/loopover-miner/lib/claim-conflict-resolver.js +++ /dev/null @@ -1,126 +0,0 @@ -// Real claim-conflict resolution (#4848): the missing piece over claim-adjudication.js's own adjudicator, -// which is correct and well-tested in isolation but has no caller that assembles a REAL competing-claims set. -// checkSubmissionFreshness (submission-freshness-check.js) already catches the common case pre-submission -- -// aborting before open_pr if another author's PR already references the issue -- but that check can only see -// what's PUBLIC at the moment it runs. Two miners racing closely enough that BOTH pass their own freshness -// check before either's PR exists yet is a genuine TOCTOU window freshness cannot close. This module is the -// POST-submission reconciliation for exactly that window: once THIS miner's PR is real and public, check -// whether ANOTHER open PR also claims the same issue and, if this miner's claim loses the election, close its -// own just-opened PR (never anyone else's) -- the write action the contributor-vs-maintainer safety framework -// keeps maintainer-only (#4833's own scope note), since it means the autonomous loop acts on a race-resolution -// decision with no human review. -// -// CLAIM-TIME ASYMMETRY (documented, not accidental): `self`'s claimedAt is the miner's OWN real local -// claim-ledger timestamp (claim-ledger.js, recorded before work even started). A competing PR's claimedAt uses -// its real GitHub `createdAt` instead -- the maintainer gate's own duplicate-winner election uses loopover -// server's "first observed this PR's linked-issue set" timestamp, but that requires a continuous, persistent -// observation history this stateless client-side tool does not have for a PR it doesn't own. `createdAt` is -// the best real, publicly-observable proxy available for someone else's PR -- live-issue-snapshot.js's own -// comment on `createdAt` explains this in more detail. -// -// EVENTUAL CONSISTENCY: this checks GitHub's live state after submission. A competing PR that exists but -// hasn't yet propagated through GitHub's own search/GraphQL indexing in the first instant would be invisible -// to a single check, so the live-state snapshot fetch is wrapped in a bounded retry-with-backoff (#6058): -// a few attempts with exponential backoff (following http-retry.js's convention), returning as soon as a -// competing claim is observed, and otherwise giving a late-propagating competitor time to surface before -// this miner is declared the winner. The write-authorization boundary (#4833) is unchanged. -import { adjudicateSoftClaim } from "./claim-adjudication.js"; -import { buildClosePrSpec } from "@loopover/engine"; -import { defaultRetryBackoffMs } from "./http-retry.js"; -// Bounded retry for the post-submission live-state check (#6058): a few attempts give a competing PR that -// hasn't propagated through GitHub's search/GraphQL index yet time to surface, without an unbounded loop. -const DEFAULT_SNAPSHOT_MAX_ATTEMPTS = 3; -const defaultSnapshotSleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); -/** - * Assemble the real competing-claims set from a fetched LiveIssueSnapshot: every OTHER open PR referencing - * the issue, excluding `selfPrNumber` and any PR authored by `minerLogin` itself (case-insensitive, mirrors - * checkSubmissionFreshness's own author comparison -- a login can be echoed back with different casing). - * Excluding same-author PRs is deliberate, not an edge case slipping through: a miner never competes against - * its own work, so if this login somehow has ANOTHER open PR on the same issue (e.g. a retry after a crash - * left a stale one behind), that PR is never treated as a competing claim to lose against -- only a genuinely - * different claimant's PR can trigger a real close. - * Pure given its inputs. - * - * @param {import("./submission-freshness-check.js").LiveIssueSnapshot | null | undefined} snapshot - * @param {number} selfPrNumber - * @param {string} minerLogin - * @returns {import("./claim-adjudication.js").ObservedClaim[]} - */ -export function assembleCompetingClaims(snapshot, selfPrNumber, minerLogin) { - const minerLoginKey = minerLogin.trim().toLowerCase(); - const referencingPrs = Array.isArray(snapshot?.referencingPrs) ? snapshot.referencingPrs : []; - return referencingPrs - .filter((pr) => pr.state === "open" && pr.number !== selfPrNumber) - .filter((pr) => typeof pr.authorLogin !== "string" || pr.authorLogin.trim().toLowerCase() !== minerLoginKey) - .map((pr) => ({ number: pr.number, claimedAt: pr.createdAt ?? null })); -} -/** - * Resolve a real claim conflict for an already-submitted PR. Fails OPEN (never closes anything) when the live - * snapshot can't be fetched -- an unavailable check is not evidence of a lost claim. - * - * @param {{ repoFullName: string, issueNumber: number, selfPrNumber: number, selfClaimedAt: string | null, minerLogin: string }} input - * @param {{ - * fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise, - * executeLocalWrite: (spec: import("@loopover/engine").LocalWriteActionSpec) => Promise, - * }} deps - * @param {{ maxAttempts?: number, sleepFn?: (ms: number) => Promise, backoffMs?: (attempt: number) => number }} [options] - * Bounded retry for the live-state snapshot fetch (#6058): up to `maxAttempts` (default 3) attempts with - * `backoffMs(attempt)` backoff between them, returning as soon as a competing claim is observed. Pure over - * the injected `sleepFn`/`backoffMs` -- no real timers in tests. - * @returns {Promise<{ - * checked: boolean, - * reason?: "live_state_unavailable", - * isWinner?: boolean, - * winnerNumber?: number | null, - * competingCount?: number, - * closeResult?: unknown, - * }>} - */ -export async function resolveClaimConflict(input, deps, options = {}) { - const maxAttempts = Number.isFinite(options.maxAttempts) && options.maxAttempts >= 1 - ? Math.floor(options.maxAttempts) - : DEFAULT_SNAPSHOT_MAX_ATTEMPTS; - const sleepFn = typeof options.sleepFn === "function" ? options.sleepFn : defaultSnapshotSleep; - const backoffMs = typeof options.backoffMs === "function" ? options.backoffMs : defaultRetryBackoffMs; - let snapshot = null; - let competing = []; - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - let current; - try { - current = await deps.fetchLiveIssueSnapshot(input.repoFullName, input.issueNumber); - } - catch { - current = null; - } - if (current && typeof current === "object") { - snapshot = current; - competing = assembleCompetingClaims(current, input.selfPrNumber, input.minerLogin); - // A competing claim observed = GitHub's index has propagated it; stop retrying and act on it now. - if (competing.length > 0) - break; - } - // Back off before the next attempt (index-propagation lag / a transient fetch failure); never after the last. - if (attempt < maxAttempts) - await sleepFn(backoffMs(attempt)); - } - if (!snapshot) { - return { checked: false, reason: "live_state_unavailable" }; - } - const adjudication = adjudicateSoftClaim({ number: input.selfPrNumber, claimedAt: input.selfClaimedAt }, competing); - if (adjudication.isWinner) { - return { checked: true, isWinner: true, winnerNumber: adjudication.winnerNumber, competingCount: competing.length }; - } - const comment = adjudication.winnerNumber - ? `Closing this PR: pull request #${adjudication.winnerNumber} claimed this issue first. This is an automated soft-claim conflict resolution -- no action needed from you.` - : `Closing this PR: another open pull request already claims this issue. This is an automated soft-claim conflict resolution -- no action needed from you.`; - const spec = buildClosePrSpec({ repoFullName: input.repoFullName, number: input.selfPrNumber, comment }); - const closeResult = await deps.executeLocalWrite(spec); - return { - checked: true, - isWinner: false, - winnerNumber: adjudication.winnerNumber, - competingCount: competing.length, - closeResult, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tY29uZmxpY3QtcmVzb2x2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjbGFpbS1jb25mbGljdC1yZXNvbHZlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwwR0FBMEc7QUFDMUcsOEdBQThHO0FBQzlHLDZHQUE2RztBQUM3Ryw2R0FBNkc7QUFDN0csMkdBQTJHO0FBQzNHLDRHQUE0RztBQUM1Ryx5R0FBeUc7QUFDekcsOEdBQThHO0FBQzlHLDhHQUE4RztBQUM5RywrR0FBK0c7QUFDL0csaUNBQWlDO0FBQ2pDLEVBQUU7QUFDRixzR0FBc0c7QUFDdEcsK0dBQStHO0FBQy9HLDJHQUEyRztBQUMzRyw2R0FBNkc7QUFDN0csNEdBQTRHO0FBQzVHLDJHQUEyRztBQUMzRyx1REFBdUQ7QUFDdkQsRUFBRTtBQUNGLHlHQUF5RztBQUN6Ryw2R0FBNkc7QUFDN0csMEdBQTBHO0FBQzFHLHlHQUF5RztBQUN6Ryx5R0FBeUc7QUFDekcsNEZBQTRGO0FBRTVGLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzlELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3BELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBS3hELDBHQUEwRztBQUMxRywwR0FBMEc7QUFDMUcsTUFBTSw2QkFBNkIsR0FBRyxDQUFDLENBQUM7QUFDeEMsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLE9BQWUsRUFBaUIsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUM7QUFFeEg7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSCxNQUFNLFVBQVUsdUJBQXVCLENBQ3JDLFFBQThDLEVBQzlDLFlBQW9CLEVBQ3BCLFVBQWtCO0lBRWxCLE1BQU0sYUFBYSxHQUFHLFVBQVUsQ0FBQyxJQUFJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-ledger-cli.d.ts b/packages/loopover-miner/lib/claim-ledger-cli.d.ts deleted file mode 100644 index 6be69757e7..0000000000 --- a/packages/loopover-miner/lib/claim-ledger-cli.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ClaimEntry, ClaimLedger, ClaimStatus } from "./claim-ledger.js"; -export type ParsedClaimClaimArgs = { - repoFullName: string; - issueNumber: number; - note: string | undefined; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; -} | { - error: string; -}; -export type ParsedClaimReleaseArgs = { - repoFullName: string; - issueNumber: number; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; -} | { - error: string; -}; -export type ParsedClaimListArgs = { - json: boolean; - repoFullName: string | null; - status: ClaimStatus | null; -} | { - error: string; -}; -export type ClaimLedgerCliOptions = { - openClaimLedger?: () => ClaimLedger; -}; -export declare function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs; -export declare function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs; -export declare function parseClaimListArgs(args: string[]): ParsedClaimListArgs; -export declare function renderClaimsTable(entries: ClaimEntry[]): string; -export declare function runClaimClaim(args: string[], options?: ClaimLedgerCliOptions): number; -export declare function runClaimRelease(args: string[], options?: ClaimLedgerCliOptions): number; -export declare function runClaimList(args: string[], options?: ClaimLedgerCliOptions): number; -export declare function runClaimCli(subcommand: string | undefined, args: string[], options?: ClaimLedgerCliOptions): number; diff --git a/packages/loopover-miner/lib/claim-ledger-cli.js b/packages/loopover-miner/lib/claim-ledger-cli.js deleted file mode 100644 index 4a77bcc777..0000000000 --- a/packages/loopover-miner/lib/claim-ledger-cli.js +++ /dev/null @@ -1,318 +0,0 @@ -import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -const CLAIM_CLAIM_USAGE = "Usage: loopover-miner claim claim [--note ] [--api-base-url ] [--dry-run] [--json]"; -const CLAIM_RELEASE_USAGE = "Usage: loopover-miner claim release [--api-base-url ] [--dry-run] [--json]"; -const CLAIM_LIST_USAGE = "Usage: loopover-miner claim list [--repo ] [--status active|released|expired] [--json]"; -function parseRepoArg(value, usage) { - if (!value) - return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -function parseIssueNumberArg(value, usage) { - if (!value) - return { error: usage }; - const parsed = Number(value); - if (!Number.isInteger(parsed) || parsed < 1) { - return { error: "issue number must be a positive integer." }; - } - return { issueNumber: parsed }; -} -export function parseClaimClaimArgs(args) { - const options = { - json: false, - note: undefined, - dryRun: false, - apiBaseUrl: undefined, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what a real claim would do and returns before opening the claim ledger at all. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--note") { - const note = args[index + 1]; - if (!note || note.startsWith("-")) { - return { error: CLAIM_CLAIM_USAGE }; - } - options.note = note; - index += 1; - continue; - } - // #5563: scope the claim to a non-default forge host, so it doesn't collide with (or get confused for) a - // same-named repo on the default github.com host. - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: CLAIM_CLAIM_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 2) { - return { error: CLAIM_CLAIM_USAGE }; - } - const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE); - if ("error" in repo) - return repo; - const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE); - if ("error" in issue) - return issue; - return { - repoFullName: repo.repoFullName, - issueNumber: issue.issueNumber, - note: options.note, - dryRun: options.dryRun, - json: options.json, - apiBaseUrl: options.apiBaseUrl, - }; -} -export function parseClaimReleaseArgs(args) { - const options = { - json: false, - dryRun: false, - apiBaseUrl: undefined, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: CLAIM_RELEASE_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 2) { - return { error: CLAIM_RELEASE_USAGE }; - } - const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE); - if ("error" in repo) - return repo; - const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE); - if ("error" in issue) - return issue; - return { - repoFullName: repo.repoFullName, - issueNumber: issue.issueNumber, - dryRun: options.dryRun, - json: options.json, - apiBaseUrl: options.apiBaseUrl, - }; -} -export function parseClaimListArgs(args) { - const options = { - json: false, - repoFullName: null, - status: null, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) { - return { error: CLAIM_LIST_USAGE }; - } - const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE); - if ("error" in repo) - return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token === "--status") { - const statusArg = args[index + 1]; - if (!statusArg || statusArg.startsWith("-")) { - return { error: CLAIM_LIST_USAGE }; - } - if (!CLAIM_STATUSES.includes(statusArg)) { - return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` }; - } - options.status = statusArg; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length > 0) { - return { error: CLAIM_LIST_USAGE }; - } - return options; -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderClaimsTable(entries) { - if (!Array.isArray(entries) || entries.length === 0) - return "no claim ledger entries"; - const header = [ - "repo".padEnd(24), - "issue".padStart(6), - "status".padEnd(10), - "claimed-at".padEnd(24), - "note".padEnd(16), - ].join(" "); - const lines = entries.map((entry) => [ - entry.repoFullName.padEnd(24), - display(entry.issueNumber).padStart(6), - entry.status.padEnd(10), - display(entry.claimedAt).padEnd(24), - display(entry.note).padEnd(16), - ].join(" ")); - return [header, ...lines].join("\n"); -} -function withClaimLedger(options, run) { - const ownsLedger = options.openClaimLedger === undefined; - const claimLedger = (options.openClaimLedger ?? openClaimLedger)(); - try { - return run(claimLedger); - } - finally { - if (ownsLedger) - claimLedger.close(); - } -} -export function runClaimClaim(args, options = {}) { - const parsed = parseClaimClaimArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, note: parsed.note ?? null }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would claim ${parsed.repoFullName}#${parsed.issueNumber}${parsed.note ? ` (note: ${parsed.note})` : ""}. No claim-ledger write was made.`); - } - return 0; - } - try { - return withClaimLedger(options, (claimLedger) => { - const claim = claimLedger.claimIssue(parsed.repoFullName, parsed.issueNumber, parsed.note, parsed.apiBaseUrl); - if (parsed.json) { - console.log(JSON.stringify({ claim }, null, 2)); - } - else { - console.log(claim.status); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runClaimRelease(args, options = {}) { - const parsed = parseClaimReleaseArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would release the claim on ${parsed.repoFullName}#${parsed.issueNumber}. No claim-ledger write was made.`); - } - return 0; - } - try { - return withClaimLedger(options, (claimLedger) => { - const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber, parsed.apiBaseUrl); - if (!claim) { - return reportCliFailure(parsed.json, "claim_not_found"); - } - if (parsed.json) { - console.log(JSON.stringify({ claim }, null, 2)); - } - else { - console.log(claim.status); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runClaimList(args, options = {}) { - const parsed = parseClaimListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return withClaimLedger(options, (claimLedger) => { - const filter = {}; - if (parsed.repoFullName !== null) - filter.repoFullName = parsed.repoFullName; - if (parsed.status !== null) - filter.status = parsed.status; - const claims = claimLedger.listClaims(filter); - if (parsed.json) { - console.log(JSON.stringify({ claims }, null, 2)); - } - else { - console.log(renderClaimsTable(claims)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runClaimCli(subcommand, args, options = {}) { - if (subcommand === "claim") - return runClaimClaim(args, options); - if (subcommand === "release") - return runClaimRelease(args, options); - if (subcommand === "list") - return runClaimList(args, options); - return reportCliFailure(argsWantJson(args), `Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsYWltLWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGNBQWMsRUFBRSxlQUFlLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUVwRSxPQUFPLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDbEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFckQsTUFBTSxpQkFBaUIsR0FDckIscUhBQXFILENBQUM7QUFDeEgsTUFBTSxtQkFBbUIsR0FDdkIsdUdBQXVHLENBQUM7QUFDMUcsTUFBTSxnQkFBZ0IsR0FDcEIsb0dBQW9HLENBQUM7QUFvQ3ZHLFNBQVMsWUFBWSxDQUFDLEtBQXlCLEVBQUUsS0FBYTtJQUM1RCxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDcEMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzdCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUyxJQUFJLENBQUMsa0JBQWtCLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ3RHLE9BQU8sRUFBRSxLQUFLLEVBQUUsd0NBQXdDLEVBQUUsQ0FBQztJQUM3RCxDQUFDO0lBQ0QsT0FBTyxFQUFFLFlBQVksRUFBRSxHQUFHLEtBQUssSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDO0FBQzlDLENBQUM7QUFFRCxTQUFTLG1CQUFtQixDQUFDLEtBQXlC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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-ledger-expiry.d.ts b/packages/loopover-miner/lib/claim-ledger-expiry.d.ts deleted file mode 100644 index 6c78c3ba71..0000000000 --- a/packages/loopover-miner/lib/claim-ledger-expiry.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** PURE — no IO, no Date, no random (#2316). */ -import type { ClaimEntry } from "./claim-ledger.js"; -export declare const DEFAULT_MAX_CLAIM_AGE_MS: number; -export type ClaimLedgerExpiryStore = { - listClaims(filter?: { - status?: "active"; - }): ClaimEntry[]; - expireClaim(repoFullName: string, issueNumber: number, apiBaseUrl?: string): ClaimEntry | null; -}; -/** - * Return active claims whose age is strictly greater than `maxAgeMs`. A claim whose age equals `maxAgeMs` exactly - * is still considered within the window (not expired). - */ -export declare function findExpiredClaims(claims: ClaimEntry[], nowMs: number, maxAgeMs: number): ClaimEntry[]; -export declare function sweepExpiredClaims(store: ClaimLedgerExpiryStore, nowMs: number, maxAgeMs?: number): ClaimEntry[]; diff --git a/packages/loopover-miner/lib/claim-ledger-expiry.js b/packages/loopover-miner/lib/claim-ledger-expiry.js deleted file mode 100644 index e3d9f54fdd..0000000000 --- a/packages/loopover-miner/lib/claim-ledger-expiry.js +++ /dev/null @@ -1,46 +0,0 @@ -/** PURE — no IO, no Date, no random (#2316). */ -export const DEFAULT_MAX_CLAIM_AGE_MS = 14 * 24 * 60 * 60 * 1000; -function claimAgeMs(claim, nowMs) { - const claimedAtMs = Date.parse(claim.claimedAt); - if (!Number.isFinite(claimedAtMs)) - return null; - return nowMs - claimedAtMs; -} -/** - * Return active claims whose age is strictly greater than `maxAgeMs`. A claim whose age equals `maxAgeMs` exactly - * is still considered within the window (not expired). - */ -export function findExpiredClaims(claims, nowMs, maxAgeMs) { - if (!Number.isFinite(nowMs) || nowMs < 0) - throw new Error("invalid_now_ms"); - if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0) - throw new Error("invalid_max_age_ms"); - if (!Array.isArray(claims)) - throw new Error("invalid_claims"); - const expired = []; - for (const claim of claims) { - if (claim?.status !== "active") - continue; - const ageMs = claimAgeMs(claim, nowMs); - // Fail closed on an unparseable claimedAt (#7732): a corrupted/hand-edited row whose age can't be computed - // is swept rather than left permanently un-expirable -- matching this module's own fail-closed posture and - // avoiding a claim that stays stuck active forever. - if (ageMs === null || ageMs > maxAgeMs) - expired.push(claim); - } - return expired; -} -export function sweepExpiredClaims(store, nowMs, maxAgeMs = DEFAULT_MAX_CLAIM_AGE_MS) { - const activeClaims = store.listClaims({ status: "active" }); - const expired = findExpiredClaims(activeClaims, nowMs, maxAgeMs); - const transitioned = []; - for (const claim of expired) { - // Echo the row's OWN apiBaseUrl back (#5563) rather than defaulting: two forge hosts can each have an - // active claim on the same owner/repo#issue, and defaulting here would expire the wrong host's row. - const updated = store.expireClaim(claim.repoFullName, claim.issueNumber, claim.apiBaseUrl); - if (updated) - transitioned.push(updated); - } - return transitioned; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tbGVkZ2VyLWV4cGlyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsYWltLWxlZGdlci1leHBpcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0RBQWdEO0FBSWhELE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUM7QUFPakUsU0FBUyxVQUFVLENBQUMsS0FBaUIsRUFBRSxLQUFhO0lBQ2xELE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQy9DLE9BQU8sS0FBSyxHQUFHLFdBQVcsQ0FBQztBQUM3QixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLGlCQUFpQixDQUFDLE1BQW9CLEVBQUUsS0FBYSxFQUFFLFFBQWdCO0lBQ3JGLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUssR0FBRyxDQUFDO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBQzVFLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJLFFBQVEsR0FBRyxDQUFDO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0lBQ3RGLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztJQUU5RCxNQUFNLE9BQU8sR0FBaUIsRUFBRSxDQUFDO0lBQ2pDLEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxFQUFFLENBQUM7UUFDM0IsSUFBSSxLQUFLLEVBQUUsTUFBTSxLQUFLLFFBQVE7WUFBRSxTQUFTO1FBQ3pDLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDdkMsMkdBQTJHO1FBQzNHLDJHQUEyRztRQUMzRyxvREFBb0Q7UUFDcEQsSUFBSSxLQUFLLEtBQUssSUFBSSxJQUFJLEtBQUssR0FBRyxRQUFRO1lBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUM5RCxDQUFDO0lBQ0QsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQztBQUVELE1BQU0sVUFBVSxrQkFBa0IsQ0FDaEMsS0FBNkIsRUFDN0IsS0FBYSxFQUNiLFdBQW1CLHdCQUF3QjtJQUUzQyxNQUFNLFlBQVksR0FBRyxLQUFLLENBQUMsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDNUQsTUFBTSxPQUFPLEdBQUcsaUJBQWlCLENBQUMsWUFBWSxFQUFFLEtBQUssRUFBRSxRQUFRLENBQUMsQ0FBQztJQUNqRSxNQUFNLFlBQVksR0FBaUIsRUFBRSxDQUFDO0lBQ3RDLEtBQUssTUFBTSxLQUFLLElBQUksT0FBTyxFQUFFLENBQUM7UUFDNUIsc0dBQXNHO1FBQ3RHLG9HQUFvRztRQUNwRyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxZQUFZLEVBQUUsS0FBSyxDQUFDLFdBQVcsRUFBRSxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0YsSUFBSSxPQUFPO1lBQUUsWUFBWSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUMxQyxDQUFDO0lBQ0QsT0FBTyxZQUFZLENBQUM7QUFDdEIsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-ledger.d.ts b/packages/loopover-miner/lib/claim-ledger.d.ts deleted file mode 100644 index 69a8ffd46b..0000000000 --- a/packages/loopover-miner/lib/claim-ledger.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -export type ClaimStatus = "active" | "released" | "expired"; -export type ClaimEntry = { - id: number; - apiBaseUrl: string; - repoFullName: string; - issueNumber: number; - claimedAt: string; - status: ClaimStatus; - note: string | null; -}; -export type RecordClaimInput = { - repoFullName: string; - issueNumber: number; - note?: string; - apiBaseUrl?: string; -}; -export type ListClaimsFilter = { - repoFullName?: string | null; - status?: ClaimStatus | null; -}; -/** Result of an atomic, concurrency-capped claim (#6758). `claimed` discriminates success (a recorded claim) - * from a cap rejection (`claim: null`); both carry the pre-insert active count and the resolved cap so a - * rejected caller can still log the violation. */ -export type ClaimWithinCapResult = { - claimed: true; - claim: ClaimEntry; - activeClaimCount: number; - maxConcurrentClaims: number; -} | { - claimed: false; - claim: null; - activeClaimCount: number; - maxConcurrentClaims: number; -}; -export type ClaimLedger = { - dbPath: string; - recordClaim(claim: RecordClaimInput): ClaimEntry; - /** Claims the issue, expiring any claim orphaned by a dead process first (#6156). */ - claimIssue(repoFullName: string, issueNumber: number, note?: string, apiBaseUrl?: string): ClaimEntry; - /** Atomically records the claim only while this repo's active-claim count is under `maxConcurrentClaims`, - * counting and inserting in one transaction so racing sibling processes can't exceed the cap (#6758). */ - claimIssueWithinCap(repoFullName: string, issueNumber: number, note: string | undefined, apiBaseUrl: string | undefined, maxConcurrentClaims: number): ClaimWithinCapResult; - /** Expire claims orphaned by a crashed/killed process, returning the transitioned rows (#6156). */ - reclaimExpiredClaims(maxAgeMs?: number): ClaimEntry[]; - releaseClaim(repoFullName: string, issueNumber: number, apiBaseUrl?: string): ClaimEntry | null; - expireClaim(repoFullName: string, issueNumber: number, apiBaseUrl?: string): ClaimEntry | null; - listClaims(filter?: ListClaimsFilter): ClaimEntry[]; - listActiveClaims(repoFullName?: string): ClaimEntry[]; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export type ReadOnlyClaimLedger = { - dbPath: string; - listActiveClaims(repoFullName: string): ClaimEntry[]; - close(): void; -}; -export declare const CLAIM_STATUSES: readonly ClaimStatus[]; -export declare function resolveClaimLedgerDbPath(env?: Record): string; -/** - * Opens the local claim ledger, creating the table on first use. `UNIQUE(api_base_url, repo_full_name, - * issue_number)` keeps ONE row per claimed issue per forge host, and `recordClaim` is a single atomic - * INSERT…ON CONFLICT statement (no read-then-write), so concurrent claims cannot duplicate a row. (#2314, #5563) - */ -export declare function openClaimLedger(dbPath?: string): ClaimLedger; -/** - * Strictly read-only ledger access for advisory-only callers (#5157) that must never write anything -- - * not even the schema-creation DDL and schema-version stamp {@link openClaimLedger} always runs on open. - * Opens the DB file in SQLite's own `readonly` mode (driver-enforced: an attempted write throws, this isn't - * just a by-convention guarantee) and touches the filesystem in no other way -- no `mkdirSync`/`chmodSync`, - * no `CREATE TABLE IF NOT EXISTS`, no migrations. The caller MUST only call this against a path it has - * already confirmed exists (e.g. via `existsSync`); a read-only connection to a nonexistent file throws. - * Throws if the expected table is missing too (a file exists at this path but isn't a real claim ledger) -- - * callers should treat that identically to any other open/query failure. - */ -export declare function openClaimLedgerReadOnly(dbPath: string): ReadOnlyClaimLedger; -export declare function recordClaim(claim: RecordClaimInput): ClaimEntry; -export declare function releaseClaim(repoFullName: string, issueNumber: number, apiBaseUrl?: string): ClaimEntry | null; -export declare function expireClaim(repoFullName: string, issueNumber: number, apiBaseUrl?: string): ClaimEntry | null; -export declare function listClaims(filter?: ListClaimsFilter): ClaimEntry[]; -/** Foundation-phase alias for `recordClaim({ repoFullName, issueNumber, note, apiBaseUrl })`. (#3351) */ -export declare function claimIssue(repoFullName: string, issueNumber: number, note?: string, apiBaseUrl?: string): ClaimEntry; -/** List only `active` claims, optionally scoped to one repo. (#3351) */ -export declare function listActiveClaims(repoFullName?: string): ClaimEntry[]; -export declare function closeDefaultClaimLedger(): void; diff --git a/packages/loopover-miner/lib/claim-ledger.js b/packages/loopover-miner/lib/claim-ledger.js deleted file mode 100644 index d17593342e..0000000000 --- a/packages/loopover-miner/lib/claim-ledger.js +++ /dev/null @@ -1,350 +0,0 @@ -import { DatabaseSync } from "node:sqlite"; -import { DEFAULT_MAX_CLAIM_AGE_MS, sweepExpiredClaims } from "./claim-ledger-expiry.js"; -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { CLAIM_LEDGER_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; -export const CLAIM_STATUSES = Object.freeze(["active", "released", "expired"]); -const defaultDbFileName = "claim-ledger.sqlite3"; -let defaultClaimLedger = null; -export function resolveClaimLedgerDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_CLAIM_LEDGER_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveClaimLedgerDbPath(), "invalid_claim_ledger_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeIssueNumber(issueNumber) { - if (!Number.isInteger(issueNumber) || issueNumber < 1) - throw new Error("invalid_issue_number"); - return issueNumber; -} -// The per-repo concurrent-claim cap the atomic count-and-claim gates on (#6758). Always an already-validated -// positive integer from the caller's MinerGoalSpec, but re-checked here because a bad value must fail loudly -// rather than silently disable the cap (a comparison against `undefined` is always false). -function normalizeMaxConcurrentClaims(maxConcurrentClaims) { - if (!Number.isInteger(maxConcurrentClaims) || maxConcurrentClaims < 1) { - throw new Error("invalid_max_concurrent_claims"); - } - return maxConcurrentClaims; -} -/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563). - * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */ -function normalizeApiBaseUrl(apiBaseUrl) { - if (apiBaseUrl === undefined || apiBaseUrl === null) - return DEFAULT_FORGE_CONFIG.apiBaseUrl; - if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) - throw new Error("invalid_api_base_url"); - return apiBaseUrl.trim(); -} -/** Optional free-text note: omitted/nullish → null; a string is kept as-is; anything else is rejected. */ -function normalizeNote(note) { - if (note === undefined || note === null) - return null; - if (typeof note !== "string") - throw new Error("invalid_note"); - return note; -} -function rowToClaim(row) { - return { - id: row.id, - apiBaseUrl: row.api_base_url, - repoFullName: row.repo_full_name, - issueNumber: row.issue_number, - claimedAt: row.claimed_at, - status: row.status, - note: row.note, - }; -} -// v1 -> v2 (#5563): scope the UNIQUE constraint by (api_base_url, repo_full_name, issue_number) instead of bare -// (repo_full_name, issue_number) -- two different forge hosts serving a same-named repo/issue must not collide -// in this ledger. SQLite cannot ALTER a UNIQUE constraint in place, so this rebuilds the table: create the new -// shape, copy every existing row with the pre-#4784 implicit single-forge default backfilled, drop the old -// table, rename the new one in. Runs inside applySchemaMigrations' own transaction, so a mid-rebuild failure -// leaves the file at v1 and retries cleanly on next open. -function addApiBaseUrlScope(db) { - db.exec(` - CREATE TABLE miner_claims_v2 ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - api_base_url TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - issue_number INTEGER NOT NULL, - claimed_at TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'released', 'expired')), - note TEXT, - UNIQUE (api_base_url, repo_full_name, issue_number) - ) - `); - // OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `status`, - // e.g. from a hand-edited or otherwise corrupted file) would violate the CHECK constraint above and abort the - // whole migration. Skipping it here is consistent with that same fail-closed posture, rather than turning one - // bad row into a permanently unmigratable file. - db.prepare(`INSERT OR IGNORE INTO miner_claims_v2 (id, api_base_url, repo_full_name, issue_number, claimed_at, status, note) - SELECT id, ?, repo_full_name, issue_number, claimed_at, status, note FROM miner_claims`).run(DEFAULT_FORGE_CONFIG.apiBaseUrl); - db.exec("DROP TABLE miner_claims"); - db.exec("ALTER TABLE miner_claims_v2 RENAME TO miner_claims"); -} -// v2 -> v3 (#4939): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of this -// same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing reads or -// writes it yet (no consumer exists until a future hosted deployment populates it). Same defensive -// column-presence guard as this file's own v1->v2 migration's sibling in portfolio-queue.js. -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(miner_claims)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE miner_claims ADD COLUMN tenant_id TEXT"); -} -/** - * Opens the local claim ledger, creating the table on first use. `UNIQUE(api_base_url, repo_full_name, - * issue_number)` keeps ONE row per claimed issue per forge host, and `recordClaim` is a single atomic - * INSERT…ON CONFLICT statement (no read-then-write), so concurrent claims cannot duplicate a row. (#2314, #5563) - */ -export function openClaimLedger(dbPath = resolveClaimLedgerDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - // LOCAL bookkeeping only: this table records which issues this miner instance has soft-claimed on this - // machine. It does NOT adjudicate contested duplicates — sibling miners claiming the same issue are - // resolved elsewhere via `isDuplicateClusterWinnerByClaim` from `@loopover/engine` (#3355). - db.exec(` - CREATE TABLE IF NOT EXISTS miner_claims ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_full_name TEXT NOT NULL, - issue_number INTEGER NOT NULL, - claimed_at TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'released', 'expired')), - note TEXT, - UNIQUE (repo_full_name, issue_number) - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addApiBaseUrlScope, addTenantIdColumn]); - // Idempotent claim in ONE atomic statement: insert a new active claim, or — only if the existing row is NOT - // already active — re-activate it (a released/expired claim can be re-claimed). The `WHERE status <> 'active'` - // guard makes re-claiming an already-active issue a true no-op (no row churn), never a duplicate row. - const recordStatement = db.prepare(` - INSERT INTO miner_claims (api_base_url, repo_full_name, issue_number, claimed_at, status, note) - VALUES (?, ?, ?, ?, 'active', ?) - ON CONFLICT(api_base_url, repo_full_name, issue_number) DO UPDATE SET - claimed_at = excluded.claimed_at, - note = excluded.note, - status = 'active' - WHERE miner_claims.status <> 'active' - `); - const getStatement = db.prepare("SELECT * FROM miner_claims WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ?"); - // RETURNING (matching portfolio-queue.js's own claim/release statements) makes the "nothing to release/expire" - // case observable directly from ONE atomic statement, rather than a separate post-UPDATE SELECT whose "row - // went missing" branch would be structurally unreachable (nothing else runs between the UPDATE and a SELECT - // on the same key within one synchronous call). - const releaseStatement = db.prepare("UPDATE miner_claims SET status = 'released' WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ? AND status = 'active' RETURNING *"); - const expireStatement = db.prepare("UPDATE miner_claims SET status = 'expired' WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ? AND status = 'active' RETURNING *"); - const listAllStatement = db.prepare("SELECT * FROM miner_claims ORDER BY id ASC"); - const listRepoStatement = db.prepare("SELECT * FROM miner_claims WHERE repo_full_name = ? ORDER BY id ASC"); - const listStatusStatement = db.prepare("SELECT * FROM miner_claims WHERE status = ? ORDER BY id ASC"); - const listRepoStatusStatement = db.prepare("SELECT * FROM miner_claims WHERE repo_full_name = ? AND status = ? ORDER BY id ASC"); - // Repo-wide active-claim tally for the atomic concurrency cap (#6758). Scoped by repo_full_name only (not - // api_base_url), matching the cross-forge counting that listActiveClaims(repoFullName) -- and the prior - // attempt-cli.js pre-check built on it -- already did, so the cap's MEANING is unchanged; only its atomicity is. - const countActiveRepoStatement = db.prepare("SELECT COUNT(*) AS count FROM miner_claims WHERE repo_full_name = ? AND status = 'active'"); - function normalizeListRepoFilter(repoFullName) { - if (repoFullName === undefined || repoFullName === null) - return undefined; - return normalizeRepoFullName(repoFullName); - } - function normalizeStatusFilter(status) { - if (status === undefined || status === null) - return undefined; - if (!CLAIM_STATUSES.includes(status)) - throw new Error("invalid_status"); - return status; - } - const ledger = { - dbPath: resolvedPath, - recordClaim(claim) { - const apiBaseUrl = normalizeApiBaseUrl(claim?.apiBaseUrl); - const repoFullName = normalizeRepoFullName(claim?.repoFullName); - const issueNumber = normalizeIssueNumber(claim?.issueNumber); - const note = normalizeNote(claim?.note); - const claimedAt = new Date().toISOString(); - recordStatement.run(apiBaseUrl, repoFullName, issueNumber, claimedAt, note); - return rowToClaim(getStatement.get(apiBaseUrl, repoFullName, issueNumber)); - }, - releaseClaim(repoFullName, issueNumber, apiBaseUrl) { - const normalizedForge = normalizeApiBaseUrl(apiBaseUrl); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const normalizedIssue = normalizeIssueNumber(issueNumber); - const row = releaseStatement.get(normalizedForge, normalizedRepo, normalizedIssue); - return row ? rowToClaim(row) : null; - }, - expireClaim(repoFullName, issueNumber, apiBaseUrl) { - const normalizedForge = normalizeApiBaseUrl(apiBaseUrl); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const normalizedIssue = normalizeIssueNumber(issueNumber); - const row = expireStatement.get(normalizedForge, normalizedRepo, normalizedIssue); - return row ? rowToClaim(row) : null; - }, - listClaims(filter = {}) { - const repoFullName = normalizeListRepoFilter(filter.repoFullName); - const status = normalizeStatusFilter(filter.status); - let rows; - if (repoFullName !== undefined && status !== undefined) { - rows = listRepoStatusStatement.all(repoFullName, status); - } - else if (repoFullName !== undefined) { - rows = listRepoStatement.all(repoFullName); - } - else if (status !== undefined) { - rows = listStatusStatement.all(status); - } - else { - rows = listAllStatement.all(); - } - return rows.map((row) => rowToClaim(row)); - }, - /** Expire claims orphaned by a crashed/killed process, returning the transitioned rows (#6156). The explicit - * counterpart to the sweep claimIssue runs on its own, mirroring reclaimStuckItems (portfolio-queue-manager.js). */ - reclaimExpiredClaims(maxAgeMs = DEFAULT_MAX_CLAIM_AGE_MS) { - return sweepExpiredClaims(ledger, Date.now(), maxAgeMs); - }, - claimIssue(repoFullName, issueNumber, note, apiBaseUrl) { - // Expire orphaned claims first, so an issue stranded 'active' by a dead process becomes claimable again - // instead of blocking indefinitely (#6156). Without this, recordClaim's `WHERE status <> 'active'` guard - // makes re-claiming an active row a no-op, so a claim whose owning process died keeps winning forever -- - // there is no other path to expireClaim. Mirrors claimNextBatch's sweep-then-claim - // (portfolio-queue-manager.js), where a lease stranded by a dead process would otherwise starve the queue. - sweepExpiredClaims(ledger, Date.now(), DEFAULT_MAX_CLAIM_AGE_MS); - return ledger.recordClaim({ repoFullName, issueNumber, note, apiBaseUrl }); - }, - /** - * Atomic, concurrency-capped claim (#6758). Sweeps orphaned claims, counts this repo's ACTIVE claims, and - * records the new claim ONLY while still strictly under `maxConcurrentClaims` -- all inside ONE `BEGIN - * IMMEDIATE` transaction. The prior enforcement split the count (attempt-cli.js's listActiveClaims) from the - * insert (claimIssue) across two statements with no shared transaction, so two sibling miner processes racing - * the same repo could both read the same sub-cap count and both claim, exceeding the cap. Fusing count + - * insert under an IMMEDIATE write lock -- with node:sqlite's shared `busy_timeout`, so the loser WAITS for the - * winner's commit rather than erroring -- closes that window: the second process sees the committed count and - * is cleanly rejected with `claimed: false` (never silently dropped), so the caller can log the cap violation. - * Returns the pre-insert `activeClaimCount` and the resolved `maxConcurrentClaims` on both paths. - */ - claimIssueWithinCap(repoFullName, issueNumber, note, apiBaseUrl, maxConcurrentClaims) { - const cap = normalizeMaxConcurrentClaims(maxConcurrentClaims); - // Normalize the repo up front: the count query keys on it, and a bad value must throw BEFORE `BEGIN` so it - // can never strand an open transaction. `issueNumber`/`note`/`apiBaseUrl` are validated by recordClaim - // INSIDE the transaction -- a bad value there is rolled back whole via the catch below. - const normalizedRepo = normalizeRepoFullName(repoFullName); - db.exec("BEGIN IMMEDIATE"); - try { - sweepExpiredClaims(ledger, Date.now(), DEFAULT_MAX_CLAIM_AGE_MS); - const activeClaimCount = countActiveRepoStatement.get(normalizedRepo).count; - if (activeClaimCount >= cap) { - // COMMIT, not ROLLBACK: a claim the sweep just expired is a legitimate transition that must persist even - // though THIS claim is rejected -- rolling back would resurrect a dead process's stale claim. - db.exec("COMMIT"); - return { claimed: false, claim: null, activeClaimCount, maxConcurrentClaims: cap }; - } - const claim = ledger.recordClaim({ repoFullName, issueNumber, note, apiBaseUrl }); - db.exec("COMMIT"); - return { claimed: true, claim, activeClaimCount, maxConcurrentClaims: cap }; - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - }, - listActiveClaims(repoFullName) { - const filter = { - status: "active", - ...(repoFullName !== undefined ? { repoFullName } : {}), - }; - return ledger.listClaims(filter); - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. Distinct from - // this store's normal claim/release/expire lifecycle: deletes every row for a repo outright. - purgeByRepo(repoFullName) { - return purgeStoreByRepo(db, CLAIM_LEDGER_PURGE_SPEC, normalizeRepoFullName(repoFullName)); - }, - close() { - db.close(); - }, - }; - return ledger; -} -/** - * Strictly read-only ledger access for advisory-only callers (#5157) that must never write anything -- - * not even the schema-creation DDL and schema-version stamp {@link openClaimLedger} always runs on open. - * Opens the DB file in SQLite's own `readonly` mode (driver-enforced: an attempted write throws, this isn't - * just a by-convention guarantee) and touches the filesystem in no other way -- no `mkdirSync`/`chmodSync`, - * no `CREATE TABLE IF NOT EXISTS`, no migrations. The caller MUST only call this against a path it has - * already confirmed exists (e.g. via `existsSync`); a read-only connection to a nonexistent file throws. - * Throws if the expected table is missing too (a file exists at this path but isn't a real claim ledger) -- - * callers should treat that identically to any other open/query failure. - */ -export function openClaimLedgerReadOnly(dbPath) { - const resolvedPath = normalizeDbPath(dbPath); - // `readOnly` (camelCase) -- node:sqlite silently IGNORES `readonly` (lowercase) as an unrecognized option - // and opens read-write anyway, defeating the entire point of this function. Verified empirically: a write - // via a `{ readonly: true }` connection succeeds with no error. - const db = new DatabaseSync(resolvedPath, { readOnly: true }); - let listActiveStatement; - try { - listActiveStatement = db.prepare("SELECT * FROM miner_claims WHERE repo_full_name = ? AND status = 'active' ORDER BY id ASC"); - } - catch (error) { - // The table doesn't exist (a file exists at this path but isn't a real claim ledger) -- close the - // connection we already opened before rethrowing, so this never leaks a file handle. - db.close(); - throw error; - } - return { - dbPath: resolvedPath, - listActiveClaims(repoFullName) { - const normalizedRepo = normalizeRepoFullName(repoFullName); - return listActiveStatement.all(normalizedRepo).map((row) => rowToClaim(row)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultClaimLedger() { - defaultClaimLedger ??= openClaimLedger(); - return defaultClaimLedger; -} -export function recordClaim(claim) { - return getDefaultClaimLedger().recordClaim(claim); -} -export function releaseClaim(repoFullName, issueNumber, apiBaseUrl) { - return getDefaultClaimLedger().releaseClaim(repoFullName, issueNumber, apiBaseUrl); -} -export function expireClaim(repoFullName, issueNumber, apiBaseUrl) { - return getDefaultClaimLedger().expireClaim(repoFullName, issueNumber, apiBaseUrl); -} -export function listClaims(filter) { - return getDefaultClaimLedger().listClaims(filter); -} -/** Foundation-phase alias for `recordClaim({ repoFullName, issueNumber, note, apiBaseUrl })`. (#3351) */ -export function claimIssue(repoFullName, issueNumber, note, apiBaseUrl) { - return getDefaultClaimLedger().claimIssue(repoFullName, issueNumber, note, apiBaseUrl); -} -/** List only `active` claims, optionally scoped to one repo. (#3351) */ -export function listActiveClaims(repoFullName) { - return getDefaultClaimLedger().listActiveClaims(repoFullName); -} -export function closeDefaultClaimLedger() { - if (!defaultClaimLedger) - return; - defaultClaimLedger.close(); - defaultClaimLedger = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tbGVkZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2xhaW0tbGVkZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLHdCQUF3QixFQUFFLGtCQUFrQixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDeEYsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDekQsT0FBTyxFQUFFLHlCQUF5QixFQUFFLGdCQUFnQixFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDeEcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDckQsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDNUQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLGdCQUFnQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFvRm5GLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUSxFQUFFLFVBQVUsRUFBRSxTQUFTLENBQUMsQ0FBMkIsQ0FBQztBQUV6RyxNQUFNLGlCQUFpQixHQUFHLHNCQUFzQixDQUFDO0FBQ2pELElBQUksa0JBQWtCLEdBQXVCLElBQUksQ0FBQztBQUVsRCxNQUFNLFVBQVUsd0JBQXdCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDNUYsT0FBTyx1QkFBdUIsQ0FBQyxpQkFBaUIsRUFBRSxnQ0FBZ0MsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUMzRixDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsTUFBaUM7SUFDeEQsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsd0JBQXdCLEVBQUUsRUFBRSw4QkFBOEIsQ0FBQyxDQUFDO0FBQ3ZHLENBQUM7QUFFRCxTQUFTLHFCQUFxQixDQUFDLFlBQXFCO0lBQ2xELElBQUksT0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/cli-error.d.ts b/packages/loopover-miner/lib/cli-error.d.ts deleted file mode 100644 index fecc3dacb5..0000000000 --- a/packages/loopover-miner/lib/cli-error.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** Shared CLI failure output (#4836): when `--json` is set, emit a parseable `{ ok: false, error }` object on - * stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */ -export declare function reportCliFailure(wantsJson: boolean, message: string, exitCode?: number): number; -/** True when argv includes `--json` (used on parse-error paths before a full parse result exists). */ -export declare function argsWantJson(args: readonly string[]): boolean; -/** Normalize a thrown value to a safe error string for CLI output. */ -export declare function describeCliError(error: unknown): string; diff --git a/packages/loopover-miner/lib/cli-error.js b/packages/loopover-miner/lib/cli-error.js deleted file mode 100644 index 8aa5e08fad..0000000000 --- a/packages/loopover-miner/lib/cli-error.js +++ /dev/null @@ -1,20 +0,0 @@ -/** Shared CLI failure output (#4836): when `--json` is set, emit a parseable `{ ok: false, error }` object on - * stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */ -export function reportCliFailure(wantsJson, message, exitCode = 2) { - if (wantsJson) { - console.log(JSON.stringify({ ok: false, error: message }, null, 2)); - } - else { - console.error(message); - } - return exitCode; -} -/** True when argv includes `--json` (used on parse-error paths before a full parse result exists). */ -export function argsWantJson(args) { - return args.includes("--json"); -} -/** Normalize a thrown value to a safe error string for CLI output. */ -export function describeCliError(error) { - return error instanceof Error ? error.message : String(error); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2xpLWVycm9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO3FHQUNxRztBQUVyRyxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsU0FBa0IsRUFBRSxPQUFlLEVBQUUsUUFBUSxHQUFHLENBQUM7SUFDaEYsSUFBSSxTQUFTLEVBQUUsQ0FBQztRQUNkLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3RFLENBQUM7U0FBTSxDQUFDO1FBQ04sT0FBTyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6QixDQUFDO0lBQ0QsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQztBQUVELHNHQUFzRztBQUN0RyxNQUFNLFVBQVUsWUFBWSxDQUFDLElBQXVCO0lBQ2xELE9BQU8sSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNqQyxDQUFDO0FBRUQsc0VBQXNFO0FBQ3RFLE1BQU0sVUFBVSxnQkFBZ0IsQ0FBQyxLQUFjO0lBQzdDLE9BQU8sS0FBSyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ2hFLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/cli.d.ts b/packages/loopover-miner/lib/cli.d.ts deleted file mode 100644 index fdfab37f46..0000000000 --- a/packages/loopover-miner/lib/cli.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export declare function printVersion(input: { - packageName: string; - packageVersion: string; -}): void; -export declare function printHelp(input: { - packageName: string; -}): void; -export declare function runCli(cliArgs: string[], input: { - packageName: string; -}): number; diff --git a/packages/loopover-miner/lib/cli.js b/packages/loopover-miner/lib/cli.js deleted file mode 100644 index e597f77858..0000000000 --- a/packages/loopover-miner/lib/cli.js +++ /dev/null @@ -1,77 +0,0 @@ -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -export function printVersion(input) { - console.log(`${input.packageName}/${input.packageVersion} (node ${process.version})`); -} -export function printHelp(input) { - console.log([ - input.packageName, - "", - "Foundation CLI for the local LoopOver miner runtime.", - "", - "Usage:", - " loopover-miner --help", - " loopover-miner --version", - " loopover-miner help", - " loopover-miner version", - " loopover-miner init [--json] [--verify-token] Bootstrap laptop-mode local SQLite state", - " loopover-miner init --interactive Guided first-run wizard: prompts for GITHUB_TOKEN + provider, writes a starter .env, then runs doctor", - " loopover-miner status [--json] Show installed versions + local state paths", - " loopover-miner doctor [--json] Check this laptop is set up correctly", - " loopover-miner migrate [--json] Apply pending schema migrations to existing local stores", - " loopover-miner metrics Print prediction-calibration counters in Prometheus text format", - " loopover-miner manage status [--json] Show managed PR rows from local portfolio + ledger", - " loopover-miner manage poll [--branch ] [--dry-run] [--json]", - " loopover-miner discover [...] [--dry-run] [--json]", - " loopover-miner discover --search [--dry-run] [--json] Fan out, rank, and enqueue candidates", - " loopover-miner attempt --miner-login [--base ] [--live] [--dry-run] [--json]", - " loopover-miner loop [...] --miner-login [--base ] [--live] [--dry-run]", - " loopover-miner loop --search --miner-login [--max-cycles ] [--cycle-delay-ms ] [--dry-run] [--json]", - " Autonomous discover->claim->attempt->reenter loop", - " loopover-miner queue list [--repo ] [--json] List portfolio backlog rows", - " loopover-miner queue next [--global-wip ] [--per-repo-wip ] [--dry-run] [--json]", - " Claim the highest-priority queued item, optionally WIP-cap-aware", - " loopover-miner queue claim-batch [--global-wip ] [--per-repo-wip ] [--dry-run] [--json]", - " loopover-miner queue metrics Print portfolio-queue counters in Prometheus text format", - " loopover-miner queue dashboard [--json] Print portfolio-queue backlog status counts + oldest-queued age", - " loopover-miner queue done [--dry-run] [--json]", - " loopover-miner queue release [--dry-run] [--json] Return a claimed item to the queue", - " loopover-miner queue requeue [--dry-run] [--json] Put a completed item back on the queue", - " loopover-miner claim claim [--note ] [--dry-run] [--json]", - " loopover-miner claim release [--dry-run] [--json]", - " loopover-miner claim list [--repo ] [--status active|released|expired] [--json]", - " loopover-miner ledger list [--repo ] [--since ] [--type ] [--json]", - " loopover-miner ledger metrics Print event-ledger counters in Prometheus text format", - " loopover-miner plan list [--status pending|running|completed|failed] [--json]", - " loopover-miner plan show [--json]", - " loopover-miner governor list [--repo ] [--type allowed|denied|throttled|kill_switch] [--json]", - " loopover-miner governor pause [--reason ] [--dry-run] [--json] Stop the loop before its next cycle", - " loopover-miner governor resume [--dry-run] [--json] Let a paused loop continue", - " loopover-miner governor status [--json] Show whether the governor is paused", - " loopover-miner governor metrics Print governor rate-limit/cap-usage counters in Prometheus text format", - " loopover-miner calibration [--json] Report predicted-vs-realized gate accuracy", - " loopover-miner feasibility [--not-found] [--json]", - " loopover-miner idea-feasibility [--not-resolvable] [--hint ]... [--json]", - " Pre-compute feasibility gate for a freeform Rent-a-Loop idea (#5671)", - " loopover-miner hooks check --tool --input [--json]", - " loopover-miner state get [--json]", - " loopover-miner state set [--dry-run] [--json]", - " loopover-miner orb export [--enable] [--send] [--dry-run] [--json] Build (and optionally send) the opt-in anonymized telemetry batch", - " loopover-miner tenant create [--product ] [--json] Provision a hosted tenant via the control-plane API", - " loopover-miner tenant list [--json] List hosted tenants", - " loopover-miner tenant destroy [--json] Tear down a hosted tenant", - " loopover-miner purge --repo [--dry-run] [--json]", - " Right-to-be-forgotten: delete a repo's rows from every local store", - "", - "Options:", - " --no-update-check Skip the npm registry version nudge (also LOOPOVER_MINER_NO_UPDATE_CHECK=1)", - " --quiet Log only warnings and errors (also LOOPOVER_MINER_LOG_LEVEL=error)", - " --verbose Log debug-level diagnostics (also LOOPOVER_MINER_LOG_LEVEL=debug)", - " --log-level Set the log level explicitly: silent|error|warn|info|debug", - ].join("\n")); -} -export function runCli(cliArgs, input) { - const command = cliArgs[0] ?? ""; - const message = `Unknown command: ${command}. Run ${input.packageName} --help.`; - return reportCliFailure(argsWantJson(cliArgs), message, 1); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVoRSxNQUFNLFVBQVUsWUFBWSxDQUFDLEtBQXNEO0lBQ2pGLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxLQUFLLENBQUMsV0FBVyxJQUFJLEtBQUssQ0FBQyxjQUFjLFVBQVUsT0FBTyxDQUFDLE9BQU8sR0FBRyxDQUFDLENBQUM7QUFDeEYsQ0FBQztBQUVELE1BQU0sVUFBVSxTQUFTLENBQUMsS0FBOEI7SUFDdEQsT0FBTyxDQUFDLEdBQUcsQ0FDVDtRQUNFLEtBQUssQ0FBQyxXQUFXO1FBQ2pCLEVBQUU7UUFDRixzREFBc0Q7UUFDdEQsRUFBRTtRQUNGLFFBQVE7UUFDUix5QkFBeUI7UUFDekIsNEJBQTRCO1FBQzVCLHVCQUF1QjtRQUN2QiwwQkFBMEI7UUFDMUIsdUdBQXVHO1FBQ3ZHLHFLQUFxSztRQUNySywyR0FBMkc7UUFDM0cscUdBQXFHO1FBQ3JHLHdIQUF3SDtRQUN4SCwrSEFBK0g7UUFDL0gsa0hBQWtIO1FBQ2xILHdGQUF3RjtRQUN4RiwrRUFBK0U7UUFDL0Usd0dBQXdHO1FBQ3hHLHNIQUFzSDtRQUN0SCxtSEFBbUg7UUFDbkgsOEhBQThIO1FBQzlILG9IQUFvSDtRQUNwSCwyRkFBMkY7UUFDM0YsMEZBQTBGO1FBQzFGLG1JQUFtSTtRQUNuSSxpR0FBaUc7UUFDakcsd0hBQXdIO1FBQ3hILCtIQUErSDtRQUMvSCw0RUFBNEU7UUFDNUUsbUhBQW1IO1FBQ25ILHVIQUF1SDtRQUN2SCx5RkFBeUY7UUFDekYsMkVBQTJFO1FBQzNFLCtGQUErRjtRQUMvRixrR0FBa0c7UUFDbEcscUhBQXFIO1FBQ3JILGlGQUFpRjtRQUNqRiw4Q0FBOEM7UUFDOUMsNkdBQTZHO1FBQzdHLDZHQUE2RztRQUM3RywwRkFBMEY7UUFDMUYsbUdBQW1HO1FBQ25HLHVJQUF1STtRQUN2SSwwR0FBMEc7UUFDMUcsd0dBQXdHO1FBQ3hHLHVIQUF1SDtRQUN2SCx1SUFBdUk7UUFDdkksb0VBQW9FO1FBQ3BFLGtEQUFrRDtRQUNsRCxvR0FBb0c7UUFDcEcsMElBQTBJO1FBQzFJLDJIQUEySDtRQUMzSCxtRkFBbUY7UUFDbkYseUZBQXlGO1FBQ3pGLGlFQUFpRTtRQUNqRSxxSUFBcUk7UUFDckksRUFBRTtRQUNGLFVBQVU7UUFDVixrR0FBa0c7UUFDbEcseUZBQXlGO1FBQ3pGLHdGQUF3RjtRQUN4RixpRkFBaUY7S0FDbEYsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQ2IsQ0FBQztBQUNKLENBQUM7QUFFRCxNQUFNLFVBQVUsTUFBTSxDQUFDLE9BQWlCLEVBQUUsS0FBOEI7SUFDdEUsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUNqQyxNQUFNLE9BQU8sR0FBRyxvQkFBb0IsT0FBTyxTQUFTLEtBQUssQ0FBQyxXQUFXLFVBQVUsQ0FBQztJQUNoRixPQUFPLGdCQUFnQixDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUMsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFDN0QsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/coding-agent-construction.d.ts b/packages/loopover-miner/lib/coding-agent-construction.d.ts deleted file mode 100644 index 63d611f4aa..0000000000 --- a/packages/loopover-miner/lib/coding-agent-construction.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type AgentSdkHooks, type AgentSdkQueryFn, type CliSubprocessSpawnFn, type CodingAgentDriver } from "@loopover/engine"; -import { type HouseRulesConfig, type HouseRulesOptions } from "./coding-agent-house-rules.js"; -/** - * Real `child_process.spawn`-backed implementation of the engine's `CliSubprocessSpawnFn` contract. Captures - * stdout/stderr and RESOLVES (never rejects) on timeout or spawn error, so the caller always sees whatever - * output accumulated rather than an unhandled rejection -- mirrors `src/selfhost/ai.ts`'s `defaultSpawn`'s own - * resolve-not-reject rationale (a killed/errored subprocess's partial output may hold the real diagnosable - * error, e.g. an auth failure line on stderr). - */ -export declare function createRealCliSubprocessSpawn(): CliSubprocessSpawnFn; -export type ConstructProductionCodingAgentDriverOptions = { - spawn?: CliSubprocessSpawnFn; - query?: AgentSdkQueryFn; - hooks?: AgentSdkHooks; - listChangedFiles?: (cwd: string) => Promise; - houseRulesConfig?: HouseRulesConfig; - houseRulesOptions?: HouseRulesOptions; -}; -/** - * Resolve `MINER_CODING_AGENT_PROVIDER` from `env` and construct a REAL, production `CodingAgentDriver` — - * house-rule-enforced by default (#2343) via `buildHouseRulesAgentSdkHooks`, matching the same - * automatic-enforcement guarantee `runHouseRulesEnforcedCodingAgentAttempt` gives task-level callers, but at - * the raw driver-construction level `attempt-runner.js`'s `deps.driver` actually needs. - * - * The default only applies to `agent-sdk`, the one provider with a real hook-registration surface. CLI - * subprocess providers (`claude-cli`/`codex-cli`) have none, and the engine's `createCliProvider` fails closed - * if `hooks` is supplied at all (driver-factory.ts) -- filling the default for them here would make every CLI - * construction throw. An explicitly-supplied `options.hooks` always wins and is forwarded as-is, so a caller - * that deliberately asks a CLI provider to enforce hooks still gets that same fail-closed rejection. - * - * Fails closed (throws) when no provider is configured, or when a CLI provider is selected without a real - * spawn available — never silently falls back to a driver that can never run. - */ -export declare function constructProductionCodingAgentDriver(env: Record, options?: ConstructProductionCodingAgentDriverOptions): CodingAgentDriver; diff --git a/packages/loopover-miner/lib/coding-agent-construction.js b/packages/loopover-miner/lib/coding-agent-construction.js deleted file mode 100644 index af30602415..0000000000 --- a/packages/loopover-miner/lib/coding-agent-construction.js +++ /dev/null @@ -1,92 +0,0 @@ -// Production coding-agent driver construction (#5131, Wave 3.5 follow-up to #2337/#2343). Closes the gap -// coding-agent-house-rules.js's own header names explicitly: "nothing in this package constructs a -// coding-agent driver in production yet ... that is separate, larger follow-up work." This module IS that -// call site -- it provides a real `child_process`-backed spawn (mirroring src/selfhost/ai.ts's `defaultSpawn`, -// simplified to the engine's smaller `CliSubprocessSpawnFn` contract: no `firstOutputTimeoutMs`/`input`, since -// those are reviewer-CLI-specific concerns this driver doesn't share) and resolves + constructs a real -// `CodingAgentDriver` from `MINER_CODING_AGENT_PROVIDER`, with house-rule enforcement (#2343) wired in by -// default via `buildHouseRulesAgentSdkHooks` -- a caller never has to remember to attach it by hand. -import { spawn as nodeSpawn } from "node:child_process"; -import { createCodingAgentDriver, resolveFirstConfiguredCodingAgentDriverName, } from "@loopover/engine"; -import { buildHouseRulesAgentSdkHooks, } from "./coding-agent-house-rules.js"; -/** - * Real `child_process.spawn`-backed implementation of the engine's `CliSubprocessSpawnFn` contract. Captures - * stdout/stderr and RESOLVES (never rejects) on timeout or spawn error, so the caller always sees whatever - * output accumulated rather than an unhandled rejection -- mirrors `src/selfhost/ai.ts`'s `defaultSpawn`'s own - * resolve-not-reject rationale (a killed/errored subprocess's partial output may hold the real diagnosable - * error, e.g. an auth failure line on stderr). - */ -export function createRealCliSubprocessSpawn() { - return (cmd, args, opts) => new Promise((resolve) => { - // `CliSubprocessSpawnFn` uses `Record` + `readonly string[]`; Node's spawn - // overloads want `ProcessEnv` + a mutable `string[]`. The cast is local and lossless (same keys/values). - const child = nodeSpawn(cmd, [...args], { - cwd: opts.cwd, - env: opts.env, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - // Unlike src/selfhost/ai.ts's defaultSpawn (a fixed ~120s default, genuinely untestable without a real - // wait), `opts.timeoutMs` here is always CALLER-supplied per CliSubprocessSpawnFn's contract -- a test can - // pass a short value against a genuinely long-lived child, so this path is exercised directly rather than - // v8-ignored. No "already settled" guard is needed: Promise resolution is idempotent (a second `resolve()` - // is a no-op) and clearing an already-fired timer is a harmless no-op too, so `close`/`error` firing after - // the timeout already resolved is safe without extra bookkeeping. - const timer = setTimeout(() => { - child.kill("SIGKILL"); - resolve({ stdout, code: null, stderr, timedOut: true }); - }, opts.timeoutMs); - child.stdout?.on("data", (chunk) => { - stdout += chunk.toString("utf8"); - }); - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString("utf8"); - }); - child.on("error", (err) => { - // A spawn-level error (e.g. ENOENT) fires before the child ever produces output, so `stderr` is always - // "" here in practice; Node guarantees this listener receives a real Error with `.message` (the - // documented contract for ChildProcess's own "error" event), so no optional chaining/fallback is needed. - clearTimeout(timer); - resolve({ stdout, code: null, stderr: err.message }); - }); - child.on("close", (code) => { - clearTimeout(timer); - resolve({ stdout, code, stderr }); - }); - }); -} -/** - * Resolve `MINER_CODING_AGENT_PROVIDER` from `env` and construct a REAL, production `CodingAgentDriver` — - * house-rule-enforced by default (#2343) via `buildHouseRulesAgentSdkHooks`, matching the same - * automatic-enforcement guarantee `runHouseRulesEnforcedCodingAgentAttempt` gives task-level callers, but at - * the raw driver-construction level `attempt-runner.js`'s `deps.driver` actually needs. - * - * The default only applies to `agent-sdk`, the one provider with a real hook-registration surface. CLI - * subprocess providers (`claude-cli`/`codex-cli`) have none, and the engine's `createCliProvider` fails closed - * if `hooks` is supplied at all (driver-factory.ts) -- filling the default for them here would make every CLI - * construction throw. An explicitly-supplied `options.hooks` always wins and is forwarded as-is, so a caller - * that deliberately asks a CLI provider to enforce hooks still gets that same fail-closed rejection. - * - * Fails closed (throws) when no provider is configured, or when a CLI provider is selected without a real - * spawn available — never silently falls back to a driver that can never run. - */ -export function constructProductionCodingAgentDriver(env, options = {}) { - const providerName = resolveFirstConfiguredCodingAgentDriverName(env); - if (!providerName) { - throw new Error("unconfigured_coding_agent_driver:no_provider_in_MINER_CODING_AGENT_PROVIDER"); - } - const hooks = options.hooks ?? - (providerName.trim().toLowerCase() === "agent-sdk" - ? buildHouseRulesAgentSdkHooks(options.houseRulesConfig, options.houseRulesOptions) - : undefined); - return createCodingAgentDriver({ - providerName, - env, - spawn: options.spawn ?? createRealCliSubprocessSpawn(), - ...(options.query !== undefined ? { query: options.query } : {}), - ...(hooks !== undefined ? { hooks } : {}), - ...(options.listChangedFiles !== undefined ? { listChangedFiles: options.listChangedFiles } : {}), - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29kaW5nLWFnZW50LWNvbnN0cnVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNvZGluZy1hZ2VudC1jb25zdHJ1Y3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEseUdBQXlHO0FBQ3pHLG1HQUFtRztBQUNuRywwR0FBMEc7QUFDMUcsK0dBQStHO0FBQy9HLCtHQUErRztBQUMvRyx1R0FBdUc7QUFDdkcsMEdBQTBHO0FBQzFHLHFHQUFxRztBQUVyRyxPQUFPLEVBQUUsS0FBSyxJQUFJLFNBQVMsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3hELE9BQU8sRUFDTCx1QkFBdUIsRUFDdkIsMkNBQTJDLEdBSzVDLE1BQU0sa0JBQWtCLENBQUM7QUFDMUIsT0FBTyxFQUNMLDRCQUE0QixHQUc3QixNQUFNLCtCQUErQixDQUFDO0FBRXZDOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSw0QkFBNEI7SUFDMUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEVBQUUsQ0FDekIsSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUN0Qix1R0FBdUc7UUFDdkcseUdBQXlHO1FBQ3pHLE1BQU0sS0FBSyxHQUFHLFNBQVMsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxFQUFFO1lBQ3RDLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBRztZQUNiLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBd0I7WUFDbEMsS0FBSyxFQUFFLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUM7U0FDbEMsQ0FBQyxDQUFDO1FBQ0gsSUFBSSxNQUFNLEdBQUcsRUFBRSxDQUFDO1FBQ2hCLElBQUksTUFBTSxHQUFHLEVBQUUsQ0FBQztRQUNoQix1R0FBdUc7UUFDdkcsMkdBQTJHO1FBQzNHLDBHQUEwRztRQUMxRywyR0FBMkc7UUFDM0csMkdBQTJHO1FBQzNHLGtFQUFrRTtRQUNsRSxNQUFNLEtBQUss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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/coding-agent-house-rules.d.ts b/packages/loopover-miner/lib/coding-agent-house-rules.d.ts deleted file mode 100644 index 3e57d0baac..0000000000 --- a/packages/loopover-miner/lib/coding-agent-house-rules.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type AgentSdkHooks, type CodingAgentDriverResult, type CodingAgentExecutionMode, type LintGuardResult, type RunCodingAgentAttemptOptions } from "@loopover/engine"; -import type { DenyRule } from "./deny-hooks.js"; -import type { appendGovernorEvent } from "./governor-ledger.js"; -export type HouseRulesConfig = { - rules?: readonly DenyRule[]; - repoFullName?: string; -}; -export type HouseRulesOptions = { - append?: typeof appendGovernorEvent; -}; -/** The concrete shape {@link buildHouseRulesAgentSdkHooks} returns -- a single PreToolUse matcher group - * holding the one house-rules callback. Structurally assignable to the engine's opaque `AgentSdkHooks`. */ -export type HouseRulesAgentSdkHooks = AgentSdkHooks & { - PreToolUse: Array<{ - hooks: Array<(input: unknown, toolUseId?: string, context?: unknown) => Promise>>; - }>; -}; -/** - * Wrap {@link buildHouseRulesPreToolUseHook}'s callback into the Claude Agent SDK's own `hooks.PreToolUse` - * registration shape (an array of matcher groups, each holding an array of hook callbacks) -- the exact - * contract `agent-sdk-driver.ts`'s own doc comment names as "#2343's stated attachment point", and the shape - * `packages/loopover-engine/test/agent-sdk-driver.test.ts` asserts is forwarded to the SDK verbatim. - */ -export declare function buildHouseRulesAgentSdkHooks(config?: HouseRulesConfig, options?: HouseRulesOptions): HouseRulesAgentSdkHooks; -/** - * Drop-in replacement for the engine's `runCodingAgentAttempt` that defaults `hooks` to - * {@link buildHouseRulesAgentSdkHooks} for the `agent-sdk` provider, so house-rule enforcement (#2343) is ON - * by default rather than opt-in. An explicitly-supplied `hooks` option always wins (e.g. a test injecting its - * own hook double, or a caller composing additional hooks of its own) -- this only fills the gap when the - * caller omitted it entirely. CLI providers (`claude-cli`, `codex-cli`) have no hook-registration surface, and - * the engine fails closed if `hooks` is supplied to them at all -- so the default is scoped to `agent-sdk` - * only; a CLI attempt with no explicit `hooks` gets none (today's inert no-op), while one that explicitly - * supplies `hooks` still gets the engine's real fail-closed rejection instead of a silently unenforced run. - */ -export declare function runHouseRulesEnforcedCodingAgentAttempt(options: RunCodingAgentAttemptOptions & { - houseRulesConfig?: HouseRulesConfig; - houseRulesOptions?: HouseRulesOptions; -}): Promise<{ - mode: CodingAgentExecutionMode; - result: CodingAgentDriverResult & { - lintGuard?: LintGuardResult; - }; -}>; diff --git a/packages/loopover-miner/lib/coding-agent-house-rules.js b/packages/loopover-miner/lib/coding-agent-house-rules.js deleted file mode 100644 index e151199887..0000000000 --- a/packages/loopover-miner/lib/coding-agent-house-rules.js +++ /dev/null @@ -1,59 +0,0 @@ -// House-rules-enforced coding-agent construction (#2343 follow-up). buildHouseRulesPreToolUseHook -// (pretooluse-hook.js) is the LIVE PreToolUse interception point, but the engine package's -// createCodingAgentDriver / runCodingAgentAttempt (driver-factory.ts) cannot import it directly -- the -// dependency only ever flows loopover-miner -> @loopover/engine, never the reverse (the engine -// package is portable and cannot depend on the miner CLI package). This module is the missing miner-side -// glue: it wraps runCodingAgentAttempt so the `agent-sdk` provider gets house-rule enforcement by DEFAULT -- -// a future real call site does not need to remember to attach it itself, closing the exact gap -// buildHouseRulesPreToolUseHook's own doc comment already anticipated ("the live interception wiring itself"). -// -// This does not build a CLI entrypoint. UPDATE (#5135/#5396): a real production call site now exists -- -// packages/loopover-miner/lib/coding-agent-construction.js's `constructProductionCodingAgentDriver` calls -// `createCodingAgentDriver` directly -- but it does NOT go through `runCodingAgentAttempt` or this module's -// own `runHouseRulesEnforcedCodingAgentAttempt` wrapper below; the real invocation path is -// `constructProductionCodingAgentDriver` -> the resulting driver becomes `IterateLoopDeps.driver` -> consumed -// by `runIterateLoop` (packages/loopover-engine/src/miner/iterate-loop.ts). House-rule enforcement for the -// live `agent-sdk` provider is instead attached directly there, via this module's own -// `buildHouseRulesAgentSdkHooks` export (called from `constructProductionCodingAgentDriver` -- so the -// DEFAULT-attachment guarantee below still holds, just through a different real call site than originally -// anticipated). `runHouseRulesEnforcedCodingAgentAttempt` itself remains real, tested, and callable, but has -// no production caller today -- it's a lower-level composable for a hypothetical caller that wants the -// non-iterate-loop path, not something this package currently needs. -import { runCodingAgentAttempt, } from "@loopover/engine"; -import { buildHouseRulesPreToolUseHook } from "./pretooluse-hook.js"; -/** - * Wrap {@link buildHouseRulesPreToolUseHook}'s callback into the Claude Agent SDK's own `hooks.PreToolUse` - * registration shape (an array of matcher groups, each holding an array of hook callbacks) -- the exact - * contract `agent-sdk-driver.ts`'s own doc comment names as "#2343's stated attachment point", and the shape - * `packages/loopover-engine/test/agent-sdk-driver.test.ts` asserts is forwarded to the SDK verbatim. - */ -export function buildHouseRulesAgentSdkHooks(config = {}, options = {}) { - return { - PreToolUse: [ - { - hooks: [ - buildHouseRulesPreToolUseHook(config, options), - ], - }, - ], - }; -} -/** - * Drop-in replacement for the engine's `runCodingAgentAttempt` that defaults `hooks` to - * {@link buildHouseRulesAgentSdkHooks} for the `agent-sdk` provider, so house-rule enforcement (#2343) is ON - * by default rather than opt-in. An explicitly-supplied `hooks` option always wins (e.g. a test injecting its - * own hook double, or a caller composing additional hooks of its own) -- this only fills the gap when the - * caller omitted it entirely. CLI providers (`claude-cli`, `codex-cli`) have no hook-registration surface, and - * the engine fails closed if `hooks` is supplied to them at all -- so the default is scoped to `agent-sdk` - * only; a CLI attempt with no explicit `hooks` gets none (today's inert no-op), while one that explicitly - * supplies `hooks` still gets the engine's real fail-closed rejection instead of a silently unenforced run. - */ -export function runHouseRulesEnforcedCodingAgentAttempt(options) { - const { houseRulesConfig, houseRulesOptions, ...attemptOptions } = options; - const hooks = attemptOptions.hooks ?? - (attemptOptions.providerName.trim().toLowerCase() === "agent-sdk" - ? buildHouseRulesAgentSdkHooks(houseRulesConfig, houseRulesOptions) - : undefined); - return runCodingAgentAttempt({ ...attemptOptions, ...(hooks !== undefined ? { hooks } : {}) }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29kaW5nLWFnZW50LWhvdXNlLXJ1bGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY29kaW5nLWFnZW50LWhvdXNlLXJ1bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGtHQUFrRztBQUNsRywyRkFBMkY7QUFDM0YsdUdBQXVHO0FBQ3ZHLCtGQUErRjtBQUMvRix5R0FBeUc7QUFDekcsNkdBQTZHO0FBQzdHLCtGQUErRjtBQUMvRiwrR0FBK0c7QUFDL0csRUFBRTtBQUNGLHdHQUF3RztBQUN4RywwR0FBMEc7QUFDMUcsNEdBQTRHO0FBQzVHLDJGQUEyRjtBQUMzRiw4R0FBOEc7QUFDOUcsMkdBQTJHO0FBQzNHLHNGQUFzRjtBQUN0RixzR0FBc0c7QUFDdEcsMEdBQTBHO0FBQzFHLDZHQUE2RztBQUM3Ryx1R0FBdUc7QUFDdkcscUVBQXFFO0FBRXJFLE9BQU8sRUFDTCxxQkFBcUIsR0FNdEIsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBQUUsNkJBQTZCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQXFCckU7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsNEJBQTRCLENBQzFDLFNBQTJCLEVBQUUsRUFDN0IsVUFBNkIsRUFBRTtJQUUvQixPQUFPO1FBQ0wsVUFBVSxFQUFFO1lBQ1Y7Z0JBQ0UsS0FBSyxFQUFFO29CQUNMLDZCQUE2QixDQUFDLE1BQU0sRUFBRSxPQUFPLENBSVI7aUJBQ3RDO2FBQ0Y7U0FDRjtLQUNGLENBQUM7QUFDSixDQUFDO0FBRUQ7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxVQUFVLHVDQUF1QyxDQUNyRCxPQUdDO0lBRUQsTUFBTSxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLEdBQUcsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBQzNFLE1BQU0sS0FBSyxHQUNULGNBQWMsQ0FBQyxLQUFLO1FBQ3BCLENBQUMsY0FBYyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsS0FBSyxXQUFXO1lBQy9ELENBQUMsQ0FBQyw0QkFBNEIsQ0FBQyxnQkFBZ0IsRUFBRSxpQkFBaUIsQ0FBQztZQUNuRSxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDakIsT0FBTyxxQkFBcUIsQ0FBQyxFQUFFLEdBQUcsY0FBYyxFQUFFLEdBQUcsQ0FBQyxLQUFLLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDakcsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/coding-task-spec.d.ts b/packages/loopover-miner/lib/coding-task-spec.d.ts deleted file mode 100644 index 3a34d43fe8..0000000000 --- a/packages/loopover-miner/lib/coding-task-spec.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { AcceptanceCriteria, FeasibilityGateResult, FeasibilityVerdict, IssueRecord, PullRequestRecord } from "@loopover/engine"; -import type { RepoStackResult } from "./stack-detection.js"; -export type CodingTaskIssue = { - number: number; - title: string; - body?: string | null | undefined; - labels?: string[] | undefined; -}; -export type CodingTaskClaimLedger = { - listClaims(filter: { - repoFullName: string; - status: string; - }): Array<{ - issueNumber: number; - }>; -}; -export type CodingTaskContext = { - issues: IssueRecord[]; - pullRequests: PullRequestRecord[]; -}; -export type CodingTaskSpecInput = { - repoFullName: string; - issue: CodingTaskIssue; - context: CodingTaskContext; - claimLedger: CodingTaskClaimLedger; - workingDirectory: string; - /** Injectable stack detector (#4786); omitted falls back to stack-detection.js's real `detectRepoStack`. */ - detectRepoStack?: (repoPath: string) => RepoStackResult; -}; -export type CodingTaskSpecResult = { - ready: false; - verdict: FeasibilityVerdict; - feasibility: FeasibilityGateResult; -} | { - ready: true; - verdict: FeasibilityVerdict; - feasibility: FeasibilityGateResult; - acceptanceCriteriaPath: string; - instructions: string; - title: string; - body: string | undefined; - labels: string[] | undefined; - linkedIssues: number[]; -}; -/** - * Compute the feasibility verdict for one target issue, from real signals: whether the issue is present in - * the fetched context, its real claim status (the claim ledger), and its real duplicate-cluster risk - * (buildCollisionReport over the fetched issues/pullRequests). issueStatus is left to its documented - * "ready" default -- see this file's header for why that's honest, not fabricated. - * - * @param {string} repoFullName - * @param {{ number: number }} issue - * @param {{ issues: Array<{ number: number }>, pullRequests: unknown[] }} context - * @param {{ listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }} claimLedger - * @returns {import("@loopover/engine").FeasibilityGateResult} - */ -export declare function buildCodingTaskFeasibility(repoFullName: string, issue: CodingTaskIssue, context: CodingTaskContext, claimLedger: CodingTaskClaimLedger): FeasibilityGateResult; -/** - * Compose the immutable AcceptanceCriteria document for one target issue + its feasibility verdict. - * - * @param {{ title: string, body?: string | null, labels?: string[] }} issue - * @param {import("@loopover/engine").FeasibilityGateResult} feasibility - * @returns {import("@loopover/engine").AcceptanceCriteria} - */ -export declare function buildCodingTaskAcceptanceCriteria(issue: CodingTaskIssue, feasibility: FeasibilityGateResult): AcceptanceCriteria; -export declare function writeAcceptanceCriteriaFile(workingDirectory: string, acceptanceCriteria: AcceptanceCriteria): { - written: boolean; - path: string | null; -}; -/** - * Full composition: feasibility -> acceptance criteria -> (if authorized) write the file -> detect the - * target-repo stack (#4786) -> instructions. Returns `ready: false` (with the computed feasibility verdict, - * for the caller to report) when the verdict is `raise`/`avoid` -- the caller should abandon the attempt - * rather than proceed with no real acceptance-criteria file on disk. - * - * `detectRepoStack` is injectable so tests can assert both the detected and fail-closed undiscovered stack - * branches without depending on real filesystem probes; omitted falls back to stack-detection.js's real - * `detectRepoStack` (the production default). - * - * @param {{ - * repoFullName: string, issue: { number: number, title: string, body?: string | null, labels?: string[] }, - * context: { issues: Array<{ number: number }>, pullRequests: unknown[] }, - * claimLedger: { listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }, - * workingDirectory: string, - * detectRepoStack?: (repoPath: string) => import("./stack-detection.js").RepoStackResult, - * }} input - * @returns {import("./coding-task-spec.js").CodingTaskSpecResult} - */ -export declare function buildCodingTaskSpec(input: CodingTaskSpecInput): CodingTaskSpecResult; diff --git a/packages/loopover-miner/lib/coding-task-spec.js b/packages/loopover-miner/lib/coding-task-spec.js deleted file mode 100644 index b77ecbc4a0..0000000000 --- a/packages/loopover-miner/lib/coding-task-spec.js +++ /dev/null @@ -1,251 +0,0 @@ -import { closeSync, constants as fsConstants, openSync, realpathSync, writeFileSync } from "node:fs"; -import { isAbsolute, join, relative } from "node:path"; -import { ACCEPTANCE_CRITERIA_FILENAME, buildAcceptanceCriteria, buildCollisionReport, buildFeasibilityVerdict, buildPromptPacket, feasibilityInputFromPreStartCheck, serializeAcceptanceCriteria, shouldWriteAcceptanceCriteria, } from "@loopover/engine"; -import { neutralizePromptInjection } from "./prompt-injection-defense.js"; -import { detectRepoStack, renderStackSummary } from "./stack-detection.js"; -// Coding-task-spec builder (#5132, Wave 3.5 follow-up). The second gap discovered alongside #5132's CLI -// wiring: `IterateLoopInput.title`/`instructions`/`acceptanceCriteriaPath` had no builder anywhere in this -// package. `packages/loopover-engine/src/miner/acceptance-criteria.ts` already composes a PromptPacket + -// FeasibilityGateResult into an immutable AcceptanceCriteria document (and deliberately does NOT write it -- -// "actually writing it into the attempt's worktree is the worktree primitive's job", per its own header) -- -// this module is that caller: derives the four inputs from a real target issue + the already-fetched -// SelfReviewContext (#5145), then writes the file for real. -// -// issueStatus is intentionally left undefined when computing feasibility: buildIssueQualityReport (the only -// thing that could supply it) lives only in root src/signals/engine.ts and has never been extracted into -// @loopover/engine (same gap #5145's own header documents for `issueQuality`). This is not a -// fabrication -- feasibilityInputFromPreStartCheck's OWN documented default for a missing -// issueQualityStatus/lifecycle is "ready", the same honest-default precedent already established. -// -// Target-repo stack detection (#4786 / #4785 follow-up): `detectRepoStack` already returned a structured -// language/package-manager/command description, but nothing in the attempt path consumed it -- instructions -// were issue text + an acceptance-criteria path only. This module now appends that real stack summary (and -// any confidently-inferred validation commands) to the coding-agent prompt so the agent validates against -// THIS repository's tooling rather than assuming LoopOver/loopover CI, Codecov, or `npm run test:ci`. -// -// Prompt-injection defense (#4795): a target issue's title/body is a customer repo's own content -- on -// Rent-a-Loop, anyone who can open an issue on that repo can shape text the coding agent later reads as -// part of its own instructions. `neutralizePromptInjection` runs on both fields before they reach either -// the coding agent's instructions (buildInstructions) or the acceptance-criteria document's taskBrief -// (buildTaskBrief) -- the two places raw issue text is embedded into agent-facing prose. This is a -// DIFFERENT concern from prompt-packet.ts's sanitizePromptPacketField (already applied downstream to -// taskBrief via buildPromptPacket): that scrubs economic/identity terms and local paths, not -// manipulation-shaped instructions, so both layers run and neither substitutes for the other. -// Emit the shared prompt_injection_neutralized audit event for any agent-facing embed of neutralized -// issue text (buildTaskBrief + buildInstructions). Kept as one helper so both call sites stay byte-identical -// (#7441 audit parity with #4795). -function logPromptInjectionNeutralized(issueNumber, title, body) { - if (!title.injected && !body.injected) - return; - console.log(JSON.stringify({ - event: "prompt_injection_neutralized", - issueNumber, - fields: [title.injected ? "title" : null, body.injected ? "body" : null].filter(Boolean), - })); -} -function buildTaskBrief(issue) { - const title = neutralizePromptInjection(issue.title); - const body = neutralizePromptInjection((issue.body ?? "").trim()); - logPromptInjectionNeutralized(issue.number, title, body); - return body.text ? `${title.text}\n\n${body.text}` : title.text; -} -function buildConstraints(issue) { - if (!Array.isArray(issue.labels) || issue.labels.length === 0) - return ""; - return `Labels on this issue: ${issue.labels.join(", ")}.`; -} -function buildFeasibilityNotes(feasibility) { - return [feasibility.summary, ...feasibility.avoidReasons, ...feasibility.raiseReasons].join("\n"); -} -// Only ever resolves to "claimed"/"unclaimed": the claim ledger's own ClaimStatus vocabulary -// ("active"|"released"|"expired") has no "solved" concept for FeasibilityClaimStatus's "solved" value to -// map from -- that would need real evidence a PR already resolved the issue (e.g. a merged, linked PR), -// which this function doesn't have access to. Not fabricated; genuinely undetectable from claim data alone. -function resolveClaimStatus(claimLedger, repoFullName, issueNumber) { - const claims = claimLedger.listClaims({ repoFullName, status: "active" }); - return claims.some((claim) => claim.issueNumber === issueNumber) ? "claimed" : "unclaimed"; -} -// The target issue's own raw cluster risk from buildCollisionReport (newly exported from -// @loopover/engine's public barrel) -- "none" when the issue isn't part of any cluster at all. -// DELIBERATELY does NOT apply #5145's ">= 2 pull_request items" threshold: that gate exists specifically to -// stop inDuplicateCluster (self-review, "does MY OWN just-created submission look redundant") from firing on -// the ordinary case of one existing PR already legitimately closing the issue. Feasibility asks a different -// question -- "should I even START working on this issue" -- where an issue already having ANY open PR -// against it (buildCollisionReport's pairwise "shared linked issue" rule, which fires at "high" for exactly -// one PR) is a meaningful, real caution signal, not a false positive to filter out. -function resolveDuplicateClusterRisk(repoFullName, issues, pullRequests, issueNumber) { - const report = buildCollisionReport(repoFullName, issues, pullRequests); - const cluster = report.clusters.find((entry) => entry.items.some((item) => item.type === "issue" && item.number === issueNumber)); - return cluster ? cluster.risk : "none"; -} -/** - * Compute the feasibility verdict for one target issue, from real signals: whether the issue is present in - * the fetched context, its real claim status (the claim ledger), and its real duplicate-cluster risk - * (buildCollisionReport over the fetched issues/pullRequests). issueStatus is left to its documented - * "ready" default -- see this file's header for why that's honest, not fabricated. - * - * @param {string} repoFullName - * @param {{ number: number }} issue - * @param {{ issues: Array<{ number: number }>, pullRequests: unknown[] }} context - * @param {{ listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }} claimLedger - * @returns {import("@loopover/engine").FeasibilityGateResult} - */ -export function buildCodingTaskFeasibility(repoFullName, issue, context, claimLedger) { - const found = context.issues.some((candidate) => candidate.number === issue.number); - const claimStatus = resolveClaimStatus(claimLedger, repoFullName, issue.number); - const duplicateClusterRisk = resolveDuplicateClusterRisk(repoFullName, context.issues, context.pullRequests, issue.number); - const feasibilityInput = feasibilityInputFromPreStartCheck({ found, claimStatus, duplicateClusterRisk }); - return buildFeasibilityVerdict(feasibilityInput); -} -/** - * Compose the immutable AcceptanceCriteria document for one target issue + its feasibility verdict. - * - * @param {{ title: string, body?: string | null, labels?: string[] }} issue - * @param {import("@loopover/engine").FeasibilityGateResult} feasibility - * @returns {import("@loopover/engine").AcceptanceCriteria} - */ -export function buildCodingTaskAcceptanceCriteria(issue, feasibility) { - const promptPacket = buildPromptPacket({ - taskBrief: buildTaskBrief(issue), - constraints: buildConstraints(issue), - feasibilityNotes: buildFeasibilityNotes(feasibility), - retrievalContext: "", - }); - return buildAcceptanceCriteria({ promptPacket, feasibility }); -} -/** - * Write the acceptance-criteria document into the prepared worktree -- only when its own verdict authorizes - * it (shouldWriteAcceptanceCriteria: verdict === "go"). A raise/avoid verdict writes nothing; the caller is - * expected to abandon the attempt rather than start it, per acceptance-criteria.ts's own documented design. - * - * @param {string} workingDirectory - * @param {import("@loopover/engine").AcceptanceCriteria} acceptanceCriteria - * @returns {{ written: boolean, path: string | null }} - */ -function assertContainedPath(root, path) { - const relativePath = relative(root, path); - if (relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath))) - return; - throw new Error(`Refusing to write acceptance criteria outside the worktree: ${path}`); -} -export function writeAcceptanceCriteriaFile(workingDirectory, acceptanceCriteria) { - if (!shouldWriteAcceptanceCriteria(acceptanceCriteria.verdict)) - return { written: false, path: null }; - const root = realpathSync(workingDirectory); - const path = join(root, ACCEPTANCE_CRITERIA_FILENAME); - assertContainedPath(root, path); - let fd; - try { - fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600); - writeFileSync(fd, serializeAcceptanceCriteria(acceptanceCriteria), "utf8"); - } - finally { - if (fd !== undefined) - closeSync(fd); - } - return { written: true, path }; -} -/** - * Prompt guidance derived from a real `detectRepoStack` result (#4786). Lists only commands the detector - * confidently inferred -- a `null` command stays omitted rather than guessed -- and always tells the agent - * not to assume LoopOver/loopover's own CI/coverage conventions. - * - * @param {import("./stack-detection.js").RepoStackResult} stack - * @returns {string} - */ -function buildValidationGuidance(stack) { - const lines = [ - `Detected target-repo stack: ${renderStackSummary(stack)}`, - "", - "Validate your change with THIS repository's own build/test/lint tooling from the stack summary above.", - "Do not assume LoopOver/loopover CI conventions, Codecov patch coverage, or `npm run test:ci` unless those commands appear in the detected stack.", - ]; - if (stack?.detected === true) { - const commands = [ - stack.testCommand ? `- test: \`${stack.testCommand}\`` : null, - stack.lintCommand ? `- lint: \`${stack.lintCommand}\`` : null, - stack.buildCommand ? `- build: \`${stack.buildCommand}\`` : null, - stack.formatCommand ? `- format: \`${stack.formatCommand}\`` : null, - ].filter((entry) => entry !== null); - if (commands.length > 0) { - lines.push("", "Run these commands before finishing:", ...commands); - } - else { - lines.push("", "No build/test/lint/format commands were confidently inferred — discover and use this repo's own tooling rather than guessing."); - } - } - return lines.join("\n"); -} -/** - * The coding-agent driver's own prompt text (agent-sdk-driver.ts's header: "forwarded verbatim as the - * prompt -- the acceptance-criteria document already lives inside the worktree", so this points to it - * rather than repeating its content). Also carries the target repo's detected stack + validation commands - * (#4786) so the agent does not default to loopover-specific CI assumptions. - * - * The issue's title/body are neutralized against prompt-injection (#4795) before embedding -- this is the - * literal `prompt:` handoff to the coding agent (agent-sdk-driver.ts), so it's the primary place untrusted - * repo content could otherwise redirect agent behavior. - * - * @param {{ number: number, title: string, body?: string | null }} issue - * @param {string} acceptanceCriteriaPath - * @param {import("./stack-detection.js").RepoStackResult} stack - */ -function buildInstructions(issue, acceptanceCriteriaPath, stack) { - const title = neutralizePromptInjection(issue.title); - const body = neutralizePromptInjection((issue.body ?? "").trim()); - logPromptInjectionNeutralized(issue.number, title, body); - return [ - `Resolve the following GitHub issue in this repository: #${issue.number} -- ${title.text}`, - "", - body.text, - "", - `A structured acceptance-criteria document describing what "done" means for this attempt is at ${acceptanceCriteriaPath} -- read it and ensure your change satisfies every criterion before finishing.`, - "", - buildValidationGuidance(stack), - ].join("\n"); -} -/** - * Full composition: feasibility -> acceptance criteria -> (if authorized) write the file -> detect the - * target-repo stack (#4786) -> instructions. Returns `ready: false` (with the computed feasibility verdict, - * for the caller to report) when the verdict is `raise`/`avoid` -- the caller should abandon the attempt - * rather than proceed with no real acceptance-criteria file on disk. - * - * `detectRepoStack` is injectable so tests can assert both the detected and fail-closed undiscovered stack - * branches without depending on real filesystem probes; omitted falls back to stack-detection.js's real - * `detectRepoStack` (the production default). - * - * @param {{ - * repoFullName: string, issue: { number: number, title: string, body?: string | null, labels?: string[] }, - * context: { issues: Array<{ number: number }>, pullRequests: unknown[] }, - * claimLedger: { listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }, - * workingDirectory: string, - * detectRepoStack?: (repoPath: string) => import("./stack-detection.js").RepoStackResult, - * }} input - * @returns {import("./coding-task-spec.js").CodingTaskSpecResult} - */ -export function buildCodingTaskSpec(input) { - const feasibility = buildCodingTaskFeasibility(input.repoFullName, input.issue, input.context, input.claimLedger); - const acceptanceCriteria = buildCodingTaskAcceptanceCriteria(input.issue, feasibility); - const writeResult = writeAcceptanceCriteriaFile(input.workingDirectory, acceptanceCriteria); - if (!writeResult.written) { - return { ready: false, verdict: feasibility.verdict, feasibility }; - } - // Real target-repo stack (#4786): detected from the prepared worktree's own manifests, not guessed from - // loopover conventions. Fail-closed `{ detected: false }` results still reach the prompt (via - // renderStackSummary) so the agent is told detection failed rather than silently defaulting to npm/Codecov. - const detect = input.detectRepoStack ?? detectRepoStack; - const stack = detect(input.workingDirectory); - const acceptanceCriteriaPath = writeResult.path; - return { - ready: true, - verdict: feasibility.verdict, - feasibility, - acceptanceCriteriaPath, - instructions: buildInstructions(input.issue, acceptanceCriteriaPath, stack), - title: input.issue.title, - body: input.issue.body ?? undefined, - labels: input.issue.labels, - linkedIssues: [input.issue.number], - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29kaW5nLXRhc2stc3BlYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNvZGluZy10YXNrLXNwZWMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLElBQUksV0FBVyxFQUFFLFFBQVEsRUFBRSxZQUFZLEVBQUUsYUFBYSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3JHLE9BQU8sRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsNEJBQTRCLEVBQzVCLHVCQUF1QixFQUN2QixvQkFBb0IsRUFDcEIsdUJBQXVCLEVBQ3ZCLGlCQUFpQixFQUNqQixpQ0FBaUMsRUFDakMsMkJBQTJCLEVBQzNCLDZCQUE2QixHQUM5QixNQUFNLGtCQUFrQixDQUFDO0FBUTFCLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQXdDM0Usd0dBQXdHO0FBQ3hHLDJHQUEyRztBQUMzRyx5R0FBeUc7QUFDekcsNkdBQTZHO0FBQzdHLDRHQUE0RztBQUM1RyxxR0FBcUc7QUFDckcsNERBQTREO0FBQzVELEVBQUU7QUFDRiw0R0FBNEc7QUFDNUcseUdBQXlHO0FBQ3pHLDZGQUE2RjtBQUM3RiwwRkFBMEY7QUFDMUYsa0dBQWtHO0FBQ2xHLEVBQUU7QUFDRix5R0FBeUc7QUFDekcsNEdBQTRHO0FBQzVHLDJHQUEyRztBQUMzRywwR0FBMEc7QUFDMUcsc0dBQXNHO0FBQ3RHLEVBQUU7QUFDRix1R0FBdUc7QUFDdkcsd0dBQXdHO0FBQ3hHLHlHQUF5RztBQUN6RyxzR0FBc0c7QUFDdEcsbUdBQW1HO0FBQ25HLHFHQUFxRztBQUNyRyw2RkFBNkY7QUFDN0YsOEZBQThGO0FBRTlGLHFHQUFxRztBQUNyRyw2R0FBNkc7QUFDN0csbUNBQW1DO0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/contribution-profile-cache.d.ts b/packages/loopover-miner/lib/contribution-profile-cache.d.ts deleted file mode 100644 index 3eff52d537..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-cache.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { CachedContributionProfile, ContributionProfile } from "./contribution-profile.js"; -export type ContributionProfileCache = { - dbPath: string; - /** Read a cached profile, or null when absent or unparseable. `stale` is true past the TTL. */ - get(repoFullName: string, nowMs?: number): CachedContributionProfile | null; - /** Cache a profile keyed by its own repoFullName, stamped with `nowMs` (defaults to now). */ - put(profile: ContributionProfile, nowMs?: number): { - repoFullName: string; - fetchedAt: string; - }; - /** Delete the cached profile for one repo (#7091); returns rows removed (0 or 1). */ - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare function resolveContributionProfileCacheDbPath(env?: Record): string; -/** - * Open the 100%-local contribution-profile cache. The DB only lives on this machine (#6797). - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations and the repo-scoped purge still use the underlying DatabaseSync until those - * helpers are migrated. Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export declare function initContributionProfileCache(dbPath?: string): ContributionProfileCache; -export declare function getCachedContributionProfile(repoFullName: string, nowMs?: number): CachedContributionProfile | null; -export declare function putCachedContributionProfile(profile: ContributionProfile, nowMs?: number): { - repoFullName: string; - fetchedAt: string; -}; -export declare function closeDefaultContributionProfileCache(): void; diff --git a/packages/loopover-miner/lib/contribution-profile-cache.js b/packages/loopover-miner/lib/contribution-profile-cache.js deleted file mode 100644 index f0310e8e73..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-cache.js +++ /dev/null @@ -1,110 +0,0 @@ -import { CONTRIBUTION_PROFILE_CACHE_TTL_MS, CONTRIBUTION_PROFILE_STORE_TABLE, } from "./contribution-profile.js"; -import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath, } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, purgeStoreByRepo, } from "./store-maintenance.js"; -const defaultDbFileName = "contribution-profile-cache.sqlite3"; -let defaultContributionProfileCache = null; -export function resolveContributionProfileCacheDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveContributionProfileCacheDbPath(), "invalid_contribution_profile_cache_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -/** - * Open the 100%-local contribution-profile cache. The DB only lives on this machine (#6797). - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations and the repo-scoped purge still use the underlying DatabaseSync until those - * helpers are migrated. Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export function initContributionProfileCache(dbPath = resolveContributionProfileCacheDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS ${CONTRIBUTION_PROFILE_STORE_TABLE} ( - repo_full_name TEXT PRIMARY KEY, - profile_json TEXT NOT NULL, - fetched_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline. No post-baseline migrations for this v1 store yet. - applySchemaMigrations(db, []); - const getSql = `SELECT profile_json, fetched_at FROM ${CONTRIBUTION_PROFILE_STORE_TABLE} WHERE repo_full_name = ?`; - const putSql = ` - INSERT INTO ${CONTRIBUTION_PROFILE_STORE_TABLE} (repo_full_name, profile_json, fetched_at) - VALUES (?, ?, ?) - ON CONFLICT(repo_full_name) DO UPDATE SET - profile_json = excluded.profile_json, - fetched_at = excluded.fetched_at - `; - return { - dbPath: resolvedPath, - /** - * Read a cached profile. Returns { profile, fetchedAt, stale } or null when absent. `stale` is true once - * the row is older than the TTL, so a caller re-extracts. A row whose JSON is unparseable is treated as a - * miss (fail closed) rather than throwing — a corrupted/hand-edited file must not break discover. - */ - get(repoFullName, nowMs = Date.now()) { - const row = driver.query(getSql, [normalizeRepoFullName(repoFullName)]).rows[0]; - if (!row) - return null; - let profile; - try { - profile = JSON.parse(row.profile_json); - } - catch { - return null; - } - const fetchedMs = Date.parse(row.fetched_at); - // An unparseable timestamp fails closed to stale, so a corrupted row is re-extracted rather than trusted. - const stale = Number.isNaN(fetchedMs) || - nowMs - fetchedMs > CONTRIBUTION_PROFILE_CACHE_TTL_MS; - return { profile, fetchedAt: row.fetched_at, stale }; - }, - /** - * Cache a profile, stamping it with the current time. The profile's own repoFullName is the key. - */ - put(profile, nowMs = Date.now()) { - const repoFullName = normalizeRepoFullName(profile?.repoFullName); - const fetchedAt = new Date(nowMs).toISOString(); - driver.query(putSql, [repoFullName, JSON.stringify(profile), fetchedAt]); - return { repoFullName, fetchedAt }; - }, - /** - * Delete the cached profile for one repo (#7091) — the right-to-be-forgotten path `loopover-miner purge` - * invokes. Returns the number of rows removed (0 or 1, since repo_full_name is the primary key). Reuses - * store-maintenance.js's identifier-guarded purgeStoreByRepo, exactly like the other repo-scoped stores. - */ - purgeByRepo(repoFullName) { - return purgeStoreByRepo(db, CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, normalizeRepoFullName(repoFullName)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultContributionProfileCache() { - defaultContributionProfileCache ??= initContributionProfileCache(); - return defaultContributionProfileCache; -} -export function getCachedContributionProfile(repoFullName, nowMs) { - return getDefaultContributionProfileCache().get(repoFullName, nowMs); -} -export function putCachedContributionProfile(profile, nowMs) { - return getDefaultContributionProfileCache().put(profile, nowMs); -} -export function closeDefaultContributionProfileCache() { - if (!defaultContributionProfileCache) - return; - defaultContributionProfileCache.close(); - defaultContributionProfileCache = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJpYnV0aW9uLXByb2ZpbGUtY2FjaGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjb250cmlidXRpb24tcHJvZmlsZS1jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFNQSxPQUFPLEVBQ0wsaUNBQWlDLEVBQ2pDLGdDQUFnQyxHQUNqQyxNQUFNLDJCQUEyQixDQUFDO0FBQ25DLE9BQU8sRUFDTCx5QkFBeUIsRUFDekIscUJBQXFCLEVBQ3JCLHVCQUF1QixHQUN4QixNQUFNLGtCQUFrQixDQUFDO0FBQzFCLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzVELE9BQU8sRUFDTCxxQ0FBcUMsRUFDckMsZ0JBQWdCLEdBQ2pCLE1BQU0sd0JBQXdCLENBQUM7QUFnQmhDLE1BQU0saUJBQWlCLEdBQUcsb0NBQW9DLENBQUM7QUFDL0QsSUFBSSwrQkFBK0IsR0FBb0MsSUFBSSxDQUFDO0FBRTVFLE1BQU0sVUFBVSxxQ0FBcUMsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUN6RyxPQUFPLHVCQUF1QixDQUM1QixpQkFBaUIsRUFDakIsOENBQThDLEVBQzlDLEdBQUcsQ0FDSixDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWlDO0lBQ3hELE9BQU8seUJBQXlCLENBQzlCLE1BQU0sRUFDTixxQ0FBcUMsRUFBRSxFQUN2Qyw0Q0FBNEMsQ0FDN0MsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFTLHFCQUFxQixDQUFDLFlBQXFCO0lBQ2xELElBQUksT0FBTyxZQUFZLEtBQUssUUFBUTtRQUNsQyxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDNUMsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUM1RCxJQUFJLENBQUMsS0FBSyxJQUFJLENB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/contribution-profile-extract.d.ts b/packages/loopover-miner/lib/contribution-profile-extract.d.ts deleted file mode 100644 index fa7fe673e7..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-extract.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ContributionProfile } from "./contribution-profile.js"; -type ExtractContributionProfileOptions = { - fetchImpl?: typeof fetch; - githubToken?: string; - apiBaseUrl?: string; - /** ISO timestamp for the profile's generatedAt; defaults to now. Injected so tests stay deterministic. */ - generatedAt?: string; - /** Sleep seam for the transient-5xx/rate-limit retry (via fetchWithRetry). Injected so tests use no real timers. */ - sleepFn?: (ms: number) => Promise; -}; -/** - * Extract a best-effort ContributionProfile for a repo from what it actually publishes. - */ -export declare function extractContributionProfile(repoFullName: string, options?: ExtractContributionProfileOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/contribution-profile-extract.js b/packages/loopover-miner/lib/contribution-profile-extract.js deleted file mode 100644 index 7ded7b9bbe..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-extract.js +++ /dev/null @@ -1,196 +0,0 @@ -import { CONTRIBUTION_PROFILE_SCHEMA_VERSION, emptyContributionProfile, weakestConfidence, } from "./contribution-profile.js"; -import { fetchWithRetry } from "./http-retry.js"; -const DEFAULT_API_BASE_URL = "https://api.github.com"; -const GITHUB_API_VERSION = "2022-11-28"; -const REQUEST_TIMEOUT_MS = 10_000; -/** A CONTRIBUTING.md smaller than this is treated as a signpost (a link to an external guide), not the rules - * themselves — #6794 found react's is 208 B and laravel' 525 B, both just pointers. */ -const CONTRIBUTING_SIGNPOST_MAX_BYTES = 600; -/** Canonical eligibility vocabulary — recognized OSS "contributor-workable" conventions. Matched case-insensitively - * as a substring over a label's name AND description. Not loopover-specific. */ -const ELIGIBILITY_TERMS = Object.freeze([ - "good first issue", - "good-first-issue", - "help wanted", - "help-wanted", - "up for grabs", - "beginner", - "easy", - "starter", -]); -/** Conventional exclusion/off-limits vocabulary. These are UNstated conventions (#6794 found no repo names - * exclusion in a label NAME explicitly), so a match yields `inferred`, never `explicit`. */ -const EXCLUSION_TERMS = Object.freeze([ - "blocked", - "on hold", - "on-hold", - "do not merge", - "wontfix", - "invalid", - "needs triage", - "work in progress", - "wip", - "maintainer only", - "internal", -]); -/** Closing-keyword / linked-issue language in a CONTRIBUTING.md. */ -const LINKED_ISSUE_TERMS = Object.freeze([ - "closes #", - "fixes #", - "resolves #", - "linked issue", - "reference an issue", - "link to an issue", -]); -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - return null; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner?.trim() || !repo?.trim() || extra !== undefined) - return null; - return { owner: owner.trim(), repo: repo.trim() }; -} -function githubHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "user-agent": "loopover-miner", - "x-github-api-version": GITHUB_API_VERSION, - }; - if (githubToken) - headers.authorization = `Bearer ${githubToken}`; - return headers; -} -/** Bounded, never-throwing JSON GET. Rides out a transient GitHub 5xx or rate-limit response (429 / secondary-403) - * via `fetchWithRetry` — the same discipline opportunity-fanout.js's sibling `githubGetJson` already uses — before - * falling back to its fail-open contract: returns null on a non-retryable/exhausted HTTP, transport, or parse - * failure. `timeoutMs` gives each attempt its own fresh `AbortSignal.timeout` (preserving the per-request bound), - * and `sleepFn` is the injectable no-real-timers seam every other `fetchWithRetry` call site exposes. */ -async function getJson(url, headers, fetchImpl, sleepFn) { - let response; - try { - // Cast: the JS always passes `sleepFn` (possibly undefined); EOPT rejects an explicit undefined optional. - response = await fetchWithRetry(fetchImpl, url, { method: "GET", headers }, { sleepFn, timeoutMs: REQUEST_TIMEOUT_MS }); - } - catch { - return null; - } - if (!response.ok) - return null; - return response.json().catch(() => null); -} -/** - * Match one label against a term list, preferring the NAME but falling back to the DESCRIPTION (the rust - * `E-easy` finding: a label can carry its eligibility meaning only in the description). Returns the matcher + - * a provenance detail, or null when neither field matches. - */ -function matchLabel(label, terms) { - const rawName = typeof label?.name === "string" ? label.name : ""; - const name = rawName.toLowerCase(); - const description = typeof label?.description === "string" - ? label.description.toLowerCase() - : ""; - const detail = rawName || "(unnamed label)"; - const nameTerm = terms.find((term) => name.includes(term)); - if (nameTerm !== undefined) - return { matcher: { field: "name", contains: nameTerm }, detail }; - const descriptionTerm = terms.find((term) => description.includes(term)); - if (descriptionTerm !== undefined) - return { - matcher: { field: "description", contains: descriptionTerm }, - detail, - }; - return null; -} -/** Classify labels into a SignalRule of the given confidence. Recognized labels build an OR-list of matchers; - * no match ⇒ `absent`. Eligibility passes `explicit` (a recognized convention IS an explicit statement); - * exclusion passes `inferred` (conventional but unstated). */ -function classifyLabels(labels, terms, matchedConfidence) { - const matchers = []; - const provenance = []; - for (const label of labels) { - const hit = matchLabel(label, terms); - if (hit === null) - continue; - matchers.push(hit.matcher); - provenance.push({ source: "labels", detail: hit.detail }); - } - if (matchers.length === 0) - return { value: null, confidence: "absent", provenance: [] }; - return { value: matchers, confidence: matchedConfidence, provenance }; -} -/** Decode a GitHub contents API response body to text. Returns null when absent or not base64. Buffer.from over - * a string never throws, so no error path is needed here. */ -function decodeContents(payload) { - if (!payload || - typeof payload !== "object" || - typeof payload.content !== "string" || - payload.encoding !== "base64") - return null; - return Buffer.from(payload.content, "base64").toString("utf8"); -} -/** Fetch CONTRIBUTING.md, probing the repo root then `.github/` (#6794: 6/10 at root, 2/10 under `.github/`). */ -async function fetchContributing(base, target, headers, fetchImpl, sleepFn) { - for (const path of ["CONTRIBUTING.md", ".github/CONTRIBUTING.md"]) { - const payload = await getJson(`${base}/repos/${target.owner}/${target.repo}/contents/${path}`, headers, fetchImpl, sleepFn); - const text = decodeContents(payload); - if (text !== null) - return text; - } - return null; -} -/** Extract the PR-body linked-issue requirement from CONTRIBUTING.md. A very small file is a signpost, not the - * rules, so it yields `absent` rather than a false negative dressed as a real one. */ -function extractPrBody(contributing) { - if (contributing === null) - return { value: null, confidence: "absent", provenance: [] }; - if (contributing.length < CONTRIBUTING_SIGNPOST_MAX_BYTES) - return { value: null, confidence: "unknown", provenance: [] }; - const lower = contributing.toLowerCase(); - const requiresLinkedIssue = LINKED_ISSUE_TERMS.some((term) => lower.includes(term)); - // A real, sufficiently-sized CONTRIBUTING.md is an explicit source either way: present-with-keyword is an - // explicit requirement, present-without is an explicit "no such rule". - return { - value: { requiresLinkedIssue }, - confidence: "explicit", - provenance: [{ source: "contributing_md", detail: "CONTRIBUTING.md" }], - }; -} -/** - * Extract a best-effort ContributionProfile for a repo from what it actually publishes. - */ -export async function extractContributionProfile(repoFullName, options = {}) { - const generatedAt = typeof options.generatedAt === "string" - ? options.generatedAt - : new Date().toISOString(); - const target = parseRepoFullName(repoFullName); - // A malformed name can't be fetched — return the safe, fully-absent default rather than throwing. - if (target === null) - return emptyContributionProfile(typeof repoFullName === "string" ? repoFullName : "", generatedAt); - /* v8 ignore next -- the global-fetch default is the production path; every test injects fetchImpl. */ - const fetchImpl = options.fetchImpl ?? fetch; - const base = typeof options.apiBaseUrl === "string" && options.apiBaseUrl.trim() - ? options.apiBaseUrl.replace(/\/+$/, "") - : DEFAULT_API_BASE_URL; - const headers = githubHeaders(options.githubToken ?? process.env.GITHUB_TOKEN); - const sleepFn = options.sleepFn; - const labelsPayload = await getJson(`${base}/repos/${target.owner}/${target.repo}/labels?per_page=100`, headers, fetchImpl, sleepFn); - const labels = Array.isArray(labelsPayload) ? labelsPayload : []; - const contributing = await fetchContributing(base, target, headers, fetchImpl, sleepFn); - const eligibilityLabels = classifyLabels(labels, ELIGIBILITY_TERMS, "explicit"); - const exclusionLabels = classifyLabels(labels, EXCLUSION_TERMS, "inferred"); - const prBody = extractPrBody(contributing); - return { - repoFullName: `${target.owner}/${target.repo}`, - schemaVersion: CONTRIBUTION_PROFILE_SCHEMA_VERSION, - generatedAt, - eligibilityLabels, - exclusionLabels, - prBody, - completeness: weakestConfidence([ - eligibilityLabels.confidence, - exclusionLabels.confidence, - prBody.confidence, - ]), - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJpYnV0aW9uLXByb2ZpbGUtZXh0cmFjdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNvbnRyaWJ1dGlvbi1wcm9maWxlLWV4dHJhY3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBY0EsT0FBTyxFQUNMLG1DQUFtQyxFQUNuQyx3QkFBd0IsRUFDeEIsaUJBQWlCLEdBQ2xCLE1BQU0sMkJBQTJCLENBQUM7QUFDbkMsT0FBTyxFQUFFLGNBQWMsRUFBOEIsTUFBTSxpQkFBaUIsQ0FBQztBQUU3RSxNQUFNLG9CQUFvQixHQUFHLHdCQUF3QixDQUFDO0FBQ3RELE1BQU0sa0JBQWtCLEdBQUcsWUFBWSxDQUFDO0FBQ3hDLE1BQU0sa0JBQWtCLEdBQUcsTUFBTSxDQUFDO0FBQ2xDO3dGQUN3RjtBQUN4RixNQUFNLCtCQUErQixHQUFHLEdBQUcsQ0FBQztBQUU1QztpRkFDaUY7QUFDakYsTUFBTSxpQkFBaUIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3RDLGtCQUFrQjtJQUNsQixrQkFBa0I7SUFDbEIsYUFBYTtJQUNiLGFBQWE7SUFDYixjQUFjO0lBQ2QsVUFBVTtJQUNWLE1BQU07SUFDTixTQUFTO0NBQ1YsQ0FBQyxDQUFDO0FBRUg7NkZBQzZGO0FBQzdGLE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDcEMsU0FBUztJQUNULFNBQVM7SUFDVCxTQUFTO0lBQ1QsY0FBYztJQUNkLFNBQVM7SUFDVCxTQUFTO0lBQ1QsY0FBYztJQUNkLGtCQUFrQjtJQUNsQixLQUFLO0lBQ0wsaUJBQWlCO0lBQ2pCLFVBQVU7Q0FDWCxDQUFDLENBQUM7QUFFSCxvRUFBb0U7QUFDcEUsTUFBTSxrQkFBa0IsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3ZDLFVBQVU7SUFDVixTQUFTO0lBQ1QsWUFBWTtJQUNaLGNBQWM7SUFDZCxvQkFBb0I7SUFDcEIsa0JBQWtCO0NBQ25CLENBQUMsQ0FBQztBQWNILFNBQVMsaUJBQWlCLENB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/contribution-profile-filter.d.ts b/packages/loopover-miner/lib/contribution-profile-filter.d.ts deleted file mode 100644 index 98cd33e560..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-filter.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ContributionProfile } from "./contribution-profile.js"; -/** Why a candidate was excluded. */ -export declare const ELIGIBILITY_EXCLUSION_REASONS: Readonly<{ - /** The issue carries a label the profile identified as maintainer-only / off-limits. */ - readonly EXCLUSION_LABEL: "exclusion_label"; - /** The repo has a trustworthy eligibility convention, and the issue carries none of its eligibility labels. */ - readonly MISSING_ELIGIBILITY_LABEL: "missing_eligibility_label"; - /** The issue carries BOTH an eligibility and an exclusion label — conflicting signals; exclusion wins. */ - readonly CONFLICTING_SIGNALS: "conflicting_signals"; - /** The issue is assigned to the repo's own owner login (#7040) — structural, not profile-derived. */ - readonly EXCLUDED_ASSIGNEE: "excluded_assignee"; -}>; -export type EligibilityExclusion = { - candidate: T; - reason: "exclusion_label" | "missing_eligibility_label" | "conflicting_signals" | "excluded_assignee"; -}; -type FilterCandidate = { - repoFullName: string; - owner?: string; - labels?: string[]; - assignees?: string[]; -}; -/** - * Partition candidates into kept + excluded against per-repo ContributionProfiles. - */ -export declare function filterCandidatesByProfiles(candidates: T[], profilesByRepo: Map): { - kept: T[]; - excluded: EligibilityExclusion[]; -}; -export {}; diff --git a/packages/loopover-miner/lib/contribution-profile-filter.js b/packages/loopover-miner/lib/contribution-profile-filter.js deleted file mode 100644 index ba2828672f..0000000000 --- a/packages/loopover-miner/lib/contribution-profile-filter.js +++ /dev/null @@ -1,112 +0,0 @@ -// Eligibility filtering of discover candidates against a ContributionProfile (#6798). Pure: given the candidate -// list and a per-repo profile map, it partitions candidates into kept + excluded-with-reason. No fetching, no -// side effects — discover-cli.js resolves the profiles and renders the result; this owns only the decision. -// -// SAFE-DEFAULT POSTURE (the load-bearing requirement) applies to the three LABEL-based rules only: filtering on -// them activates ONLY when a repo's profile has a trustworthy eligibility signal -// (eligibilityLabels.confidence === "explicit"). A repo with no profile, or a low-confidence/empty one — a repo -// whose conventions AMS simply couldn't read — has every candidate kept via those rules, so a weak profile can -// never cause AMS to silently skip real, eligible work. -// -// ASSIGNEE-EXCLUSION IS DIFFERENT (#7040): per the schema (ContributionAssigneeRuntimeCheck, -// contribution-profile.d.ts), it is deliberately NOT a profile field — it's a structural fact derivable from the -// issue's own assignees at query time, not something extraction infers with variable confidence. It therefore -// applies to EVERY candidate unconditionally, independent of the repo's ContributionProfile (or lack of one). -/** Why a candidate was excluded. */ -export const ELIGIBILITY_EXCLUSION_REASONS = Object.freeze({ - /** The issue carries a label the profile identified as maintainer-only / off-limits. */ - EXCLUSION_LABEL: "exclusion_label", - /** The repo has a trustworthy eligibility convention, and the issue carries none of its eligibility labels. */ - MISSING_ELIGIBILITY_LABEL: "missing_eligibility_label", - /** The issue carries BOTH an eligibility and an exclusion label — conflicting signals; exclusion wins. */ - CONFLICTING_SIGNALS: "conflicting_signals", - /** The issue is assigned to the repo's own owner login (#7040) — structural, not profile-derived. */ - EXCLUDED_ASSIGNEE: "excluded_assignee", -}); -/** True when the candidate is assigned to its own repo's owner login (case-insensitive). Always-on: unlike the - * label rules below, this never depends on the profile's confidence — see the header comment. */ -function isAssignedToRepoOwner(candidate) { - const owner = typeof candidate?.owner === "string" ? candidate.owner.toLowerCase() : ""; - if (!owner) - return false; - for (const login of candidate?.assignees ?? []) { - if (typeof login === "string" && login.toLowerCase() === owner) - return true; - } - return false; -} -/** The actual repo label names a signal rule was derived from (its provenance details), lowercased for match. */ -function labelNamesFromRule(rule) { - const names = new Set(); - for (const entry of rule?.provenance ?? []) { - if (typeof entry?.detail === "string") - names.add(entry.detail.toLowerCase()); - } - return names; -} -/** Does the candidate carry any label whose name is in `names`? Case-insensitive. */ -function candidateHasAnyLabel(candidate, names) { - if (names.size === 0) - return false; - for (const label of candidate?.labels ?? []) { - if (typeof label === "string" && names.has(label.toLowerCase())) - return true; - } - return false; -} -/** - * Partition candidates into kept + excluded against per-repo ContributionProfiles. - */ -export function filterCandidatesByProfiles(candidates, profilesByRepo) { - const kept = []; - const excluded = []; - for (const candidate of candidates) { - // Always-on, ahead of the label rules' safe-default gate (#7040) — see the header comment. - if (isAssignedToRepoOwner(candidate)) { - excluded.push({ - candidate, - reason: ELIGIBILITY_EXCLUSION_REASONS.EXCLUDED_ASSIGNEE, - }); - continue; - } - // Optional chaining preserved from the JS (tests may inject a nullish map via cast). - const profile = profilesByRepo?.get(candidate.repoFullName); - // Trust gate: only an EXPLICIT eligibility signal is trustworthy enough to filter on. Anything weaker - // (absent/inferred/unknown, or no profile at all) keeps every candidate — the safe default. - if (profile?.eligibilityLabels?.confidence !== "explicit") { - kept.push(candidate); - continue; - } - const eligibilityNames = labelNamesFromRule(profile.eligibilityLabels); - const exclusionNames = labelNamesFromRule(profile.exclusionLabels); - const hasEligibility = candidateHasAnyLabel(candidate, eligibilityNames); - const hasExclusion = candidateHasAnyLabel(candidate, exclusionNames); - if (hasExclusion && hasEligibility) { - // Conservative resolution for conflicting signals: exclusion wins. A maintainer marking an issue - // off-limits outranks its also carrying an eligibility label — better to skip than to attempt work the - // repo's own gate would reject. - excluded.push({ - candidate, - reason: ELIGIBILITY_EXCLUSION_REASONS.CONFLICTING_SIGNALS, - }); - continue; - } - if (hasExclusion) { - excluded.push({ - candidate, - reason: ELIGIBILITY_EXCLUSION_REASONS.EXCLUSION_LABEL, - }); - continue; - } - if (!hasEligibility) { - excluded.push({ - candidate, - reason: ELIGIBILITY_EXCLUSION_REASONS.MISSING_ELIGIBILITY_LABEL, - }); - continue; - } - kept.push(candidate); - } - return { kept, excluded }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJpYnV0aW9uLXByb2ZpbGUtZmlsdGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY29udHJpYnV0aW9uLXByb2ZpbGUtZmlsdGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGdIQUFnSDtBQUNoSCw4R0FBOEc7QUFDOUcsNEdBQTRHO0FBQzVHLEVBQUU7QUFDRixnSEFBZ0g7QUFDaEgsaUZBQWlGO0FBQ2pGLGdIQUFnSDtBQUNoSCwrR0FBK0c7QUFDL0csd0RBQXdEO0FBQ3hELEVBQUU7QUFDRiw2RkFBNkY7QUFDN0YsaUhBQWlIO0FBQ2pILDhHQUE4RztBQUM5Ryw4R0FBOEc7QUFJOUcsb0NBQW9DO0FBQ3BDLE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDekQsd0ZBQXdGO0lBQ3hGLGVBQWUsRUFBRSxpQkFBaUI7SUFDbEMsK0dBQStHO0lBQy9HLHlCQUF5QixFQUFFLDJCQUEyQjtJQUN0RCwwR0FBMEc7SUFDMUcsbUJBQW1CLEVBQUUscUJBQXFCO0lBQzFDLHFHQUFxRztJQUNyRyxpQkFBaUIsRUFBRSxtQkFBbUI7Q0FDOUIsQ0FBQyxDQUFDO0FBa0JaO2tHQUNrRztBQUNsRyxTQUFTLHFCQUFxQixDQUFDLFNBQTBCO0lBQ3ZELE1BQU0sS0FBSyxHQUFHLE9BQU8sU0FBUyxFQUFFLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsV0FBVyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUN4RixJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ3pCLEtBQUssTUFBTSxLQUFLLElBQUksU0FBUyxFQUFFLFNBQVMsSUFBSSxFQUFFLEVBQUUsQ0FBQztRQUMvQyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsV0FBVyxFQUFFLEtBQUssS0FBSztZQUFFLE9BQU8sSUFBSSxDQUFDO0lBQzlF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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/contribution-profile.d.ts b/packages/loopover-miner/lib/contribution-profile.d.ts deleted file mode 100644 index 603eeca765..0000000000 --- a/packages/loopover-miner/lib/contribution-profile.d.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** How trustworthy a single extracted rule is. `explicit`: derived from an unambiguous, machine-readable - * signal (a label whose name/description states eligibility, a CONTRIBUTING line that names a required label). - * `inferred`: derived from a conventional-but-unstated signal (a `blocked` status label read as exclusionary). - * `absent`: the repo exposes no signal of this kind at all — a real, common answer (3/10 of the #6794 sample - * had no eligibility label), and deliberately distinct from `unknown`. */ -export type ContributionSignalConfidence = "explicit" | "inferred" | "absent" | "unknown"; -/** Where a rule was derived from, for debuggability (#6794 found the primary source differs per repo — some - * state rules only in agent docs, some only in labels). */ -export type ContributionSignalSource = "labels" | "contributing_md" | "pr_template" | "agent_docs"; -export interface ContributionSignalProvenance { - source: ContributionSignalSource; - /** Human-readable pointer to the exact signal, e.g. a label name or a doc path. Never secrets. */ - detail: string; -} -/** A matcher for an eligibility/exclusion label. Matches over the label's NAME or DESCRIPTION — #6794 found - * rust encodes "good first issue" semantics only in `E-easy`'s description, which a name-only match misses. */ -export interface ContributionLabelMatcher { - /** Which field the pattern tests. */ - field: "name" | "description"; - /** Case-insensitive substring the field must contain (not a regex — kept simple and auditable). */ - contains: string; -} -/** One extracted rule: its value, how confident the extractor was, and what it was derived from. `value` is - * `null` when `confidence` is `absent`/`unknown`, so a consumer never mistakes "no rule" for "empty rule". */ -export interface ContributionSignalRule { - value: T | null; - confidence: ContributionSignalConfidence; - provenance: ContributionSignalProvenance[]; -} -/** Optional PR-body requirements. Modelled as an optional slot rather than a spine field precisely because - * #6794 found the linked-issue requirement is loopover-local, not an ecosystem norm. */ -export interface ContributionPrBodyRequirements { - /** Does a PR need to reference an issue with a closing keyword (Closes/Fixes #N)? */ - requiresLinkedIssue: boolean; -} -/** The learned contribution-eligibility profile for one repo. */ -export interface ContributionProfile { - repoFullName: string; - /** Bumped when the field set/semantics change, so a cached profile from an older extractor is detectable. */ - schemaVersion: number; - /** ISO timestamp the profile was built. */ - generatedAt: string; - /** Which label(s) mark an issue contributor-workable. `value` is an OR-list of matchers; `absent` when the - * repo exposes no eligibility label (a real outcome for 3/10 of the #6794 sample). */ - eligibilityLabels: ContributionSignalRule; - /** Which label(s) mark an issue maintainer-only / off-limits. Weaker/more inferential than eligibility per - * #6794 (nothing in the sample named exclusion in a label NAME), hence usually `inferred` or `absent`. */ - exclusionLabels: ContributionSignalRule; - /** Optional PR-body requirements (see the type). Absent for most repos. */ - prBody: ContributionSignalRule; - /** Overall completeness: the least-confident spine signal, so `discover` can treat a partial profile - * conservatively. NOT an average — one strong signal must not mask an absent one. */ - completeness: ContributionSignalConfidence; -} -/** Assignee-exclusion (e.g. "not assigned to the repo owner") is deliberately NOT a profile field: #6794 found - * it is not documented for most repos and is derivable from the issue's own `assignees` at query time. This - * type names that runtime check so the implementation issues (#6796/#6798) treat it as a live filter, not a - * cached rule. */ -export interface ContributionAssigneeRuntimeCheck { - /** Exclude issues assigned to any of these logins (typically the repo owner). Applied at discover time. */ - excludeAssignedLogins: string[]; -} -/** A cached profile plus the metadata that governs when it is refreshed. Mirrors the miner's other local - * SQLite stores (policy-doc-cache.js): keyed by repo, with a TTL, because labels and docs both change. */ -export interface CachedContributionProfile { - profile: ContributionProfile; - /** ISO timestamp the profile was written to the cache. */ - fetchedAt: string; - /** True once `fetchedAt` is older than the store's TTL — the caller should re-extract. */ - stale: boolean; -} -/** Bumped when the field set/semantics change, so a cached profile from an older extractor is detectable. */ -export declare const CONTRIBUTION_PROFILE_SCHEMA_VERSION: 1; -/** Confidence vocabulary, weakest-last order used by weakestConfidence. `absent` (the repo has no such signal) - * is deliberately distinct from `unknown` (we have not looked / could not tell). */ -export declare const CONTRIBUTION_SIGNAL_CONFIDENCE_LEVELS: readonly ["explicit", "inferred", "absent", "unknown"]; -/** The signal sources a rule can be derived from (#6794 found the primary source differs per repo). */ -export declare const CONTRIBUTION_SIGNAL_SOURCES: readonly ["labels", "contributing_md", "pr_template", "agent_docs"]; -/** Default cache TTL: 7 days. Labels/docs change slowly; a week bounds staleness without re-fetching per run. */ -export declare const CONTRIBUTION_PROFILE_CACHE_TTL_MS: number; -/** The local SQLite store table the cache (#6797) will use, named here so the schema owns it. */ -export declare const CONTRIBUTION_PROFILE_STORE_TABLE: "miner_contribution_profile"; -/** - * Build an empty, fully-`absent` profile for a repo — the safe default before extraction has run, so `discover` - * treats an unprofiled repo conservatively rather than as "no restrictions". - */ -export declare function emptyContributionProfile(repoFullName: string, generatedAt: string): ContributionProfile; -/** - * The least-confident of a set of signal confidences — the rule behind a profile's `completeness`. Weakest - * wins, so one strong signal never masks an absent one. An empty set is `unknown` (nothing observed). - */ -export declare function weakestConfidence(confidences: readonly ContributionSignalConfidence[]): ContributionSignalConfidence; diff --git a/packages/loopover-miner/lib/contribution-profile.js b/packages/loopover-miner/lib/contribution-profile.js deleted file mode 100644 index 74aba0fad3..0000000000 --- a/packages/loopover-miner/lib/contribution-profile.js +++ /dev/null @@ -1,60 +0,0 @@ -// ContributionProfile schema constants + tiny pure helpers (#6795). Design/schema only — no extraction logic -// (#6796) and no `discover` wiring (#6798) live here. The shapes are documented in contribution-profile.d.ts -// and packages/loopover-miner/docs/contribution-profile.md; this file exists so the implementation issues have -// concrete, importable constants and the two branch-free helpers they will build on. -/** Bumped when the field set/semantics change, so a cached profile from an older extractor is detectable. */ -export const CONTRIBUTION_PROFILE_SCHEMA_VERSION = 1; -/** Confidence vocabulary, weakest-last order used by weakestConfidence. `absent` (the repo has no such signal) - * is deliberately distinct from `unknown` (we have not looked / could not tell). */ -export const CONTRIBUTION_SIGNAL_CONFIDENCE_LEVELS = Object.freeze([ - "explicit", - "inferred", - "absent", - "unknown", -]); -/** The signal sources a rule can be derived from (#6794 found the primary source differs per repo). */ -export const CONTRIBUTION_SIGNAL_SOURCES = Object.freeze([ - "labels", - "contributing_md", - "pr_template", - "agent_docs", -]); -/** Default cache TTL: 7 days. Labels/docs change slowly; a week bounds staleness without re-fetching per run. */ -export const CONTRIBUTION_PROFILE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; -/** The local SQLite store table the cache (#6797) will use, named here so the schema owns it. */ -export const CONTRIBUTION_PROFILE_STORE_TABLE = "miner_contribution_profile"; -/** An `absent` signal rule with no value and no provenance — the safe default for a spine field. */ -function absentRule() { - return { value: null, confidence: "absent", provenance: [] }; -} -/** - * Build an empty, fully-`absent` profile for a repo — the safe default before extraction has run, so `discover` - * treats an unprofiled repo conservatively rather than as "no restrictions". - */ -export function emptyContributionProfile(repoFullName, generatedAt) { - return { - repoFullName, - schemaVersion: CONTRIBUTION_PROFILE_SCHEMA_VERSION, - generatedAt, - eligibilityLabels: absentRule(), - exclusionLabels: absentRule(), - prBody: absentRule(), - completeness: "absent", - }; -} -/** - * The least-confident of a set of signal confidences — the rule behind a profile's `completeness`. Weakest - * wins, so one strong signal never masks an absent one. An empty set is `unknown` (nothing observed). - */ -export function weakestConfidence(confidences) { - let weakestIndex = -1; - for (const confidence of confidences) { - const index = CONTRIBUTION_SIGNAL_CONFIDENCE_LEVELS.indexOf(confidence); - if (index > weakestIndex) - weakestIndex = index; - } - return weakestIndex === -1 - ? "unknown" - : CONTRIBUTION_SIGNAL_CONFIDENCE_LEVELS[weakestIndex]; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJpYnV0aW9uLXByb2ZpbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjb250cmlidXRpb24tcHJvZmlsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw2R0FBNkc7QUFDN0csNkdBQTZHO0FBQzdHLCtHQUErRztBQUMvRyxxRkFBcUY7QUErRnJGLDZHQUE2RztBQUM3RyxNQUFNLENBQUMsTUFBTSxtQ0FBbUMsR0FBRyxDQUFVLENBQUM7QUFFOUQ7cUZBQ3FGO0FBQ3JGLE1BQU0sQ0FBQyxNQUFNLHFDQUFxQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDakUsVUFBVTtJQUNWLFVBQVU7SUFDVixRQUFRO0lBQ1IsU0FBUztDQUNELENBQUMsQ0FBQztBQUVaLHVHQUF1RztBQUN2RyxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3ZELFFBQVE7SUFDUixpQkFBaUI7SUFDakIsYUFBYTtJQUNiLFlBQVk7Q0FDSixDQUFDLENBQUM7QUFFWixpSEFBaUg7QUFDakgsTUFBTSxDQUFDLE1BQU0saUNBQWlDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUV6RSxpR0FBaUc7QUFDakcsTUFBTSxDQUFDLE1BQU0sZ0NBQWdDLEdBQUcsNEJBQXFDLENBQUM7QUFFdEYsb0dBQW9HO0FBQ3BHLFNBQVMsVUFBVTtJQUNqQixPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxFQUFFLEVBQUUsQ0FBQztBQUMvRCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLHdCQUF3QixDQUFDLFlBQW9CLEVBQUUsV0FBbUI7SUFDaEYsT0FBTztRQUNMLFlBQVk7UUFDWixhQUFhLEVBQUUsbUNBQW1DO1FBQ2xELFdBQVc7UUFDWCxpQkFBaUIsRUFBRSxVQUFVLEVBQUU7UUFDL0IsZUFBZSxFQUFFLFVBQVUsRUFBRTtRQUM3QixNQUFNLEVBQUUsVUFBVSxFQUFFO1FBQ3BCLFlBQVksRUFBRSxRQUFRO0tBQ3ZCLENBQUM7QUFDSixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLGlCQUFpQixDQUMvQixXQUFvRDtJQUVwRCxJQUFJLFlBQVksR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN0QixLQUFLLE1BQU0sVUFBVSxJQUFJLFdBQVcsRUFBRSxDQUFDO1FBQ3JDLE1BQU0sS0FBSyxHQUFJLHFDQUEyRCxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUMvRixJQUFJLEtBQUssR0FBRyxZQUFZO1lBQUUsWUFBWSxHQUFHLEtBQUssQ0FBQztJQUNqRCxDQUFDO0lBQ0QsT0FBTyxZQUFZLEtBQUssQ0FBQyxDQUFDO1FBQ3hCLENBQUMsQ0FBQyxTQUFTO1FBQ1gsQ0FBQyxDQUFDLHFDQUFxQyxDQUFDLFlBQVksQ0FBRSxDQUFDO0FBQzNELENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/cross-repo-evaluation.d.ts b/packages/loopover-miner/lib/cross-repo-evaluation.d.ts deleted file mode 100644 index a074033adf..0000000000 --- a/packages/loopover-miner/lib/cross-repo-evaluation.d.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { RepoStackResult } from "./stack-detection.js"; -/** Failure taxonomy surfaced in per-repo reports (#4788). */ -export declare const CROSS_REPO_FAILURE_CATEGORY: Readonly<{ - STACK_DETECTION: "stack_detection_gap"; - EXECUTION: "execution_gap"; - GITTENSOR_ASSUMPTION: "loopover_assumption"; - CLONE_SETUP: "clone_setup"; - OTHER: "other"; -}>; -/** Instruction substrings that indicate a POSITIVE loopover/LoopOver CI assumption leaked into the agent prompt. - * Lines that explicitly tell the agent *not* to assume these are filtered out before scanning. */ -export declare const GITTENSOR_POSITIVE_ASSUMPTION_CHECKS: ReadonlyArray<{ - id: string; - pattern: RegExp; -}>; -export declare const DEFAULT_CROSS_REPO_MANIFEST_RELATIVE_PATH: string; -export declare const MAX_CROSS_REPO_MANIFEST_BYTES: number; -export declare const MAX_CROSS_REPO_MANIFEST_REPOS: number; -export type CrossRepoEvaluationManifestRepo = { - repoFullName: string; - stackHint?: string; - requireTestCommand?: boolean; - fixturePath?: string; -}; -export type ParsedCrossRepoEvaluationManifest = { - present: boolean; - manifest: { - repos: CrossRepoEvaluationManifestRepo[]; - }; - warnings: string[]; -}; -export type CrossRepoEvaluationResult = { - repoFullName: string; - passed: boolean; - failureCategory: string | null; - reason: string | null; - stackDetected: boolean; - usedDefaultGoalSpec: boolean | null; - assumptionFindings: Array<{ - id: string; - line: string; - }>; - stack?: RepoStackResult; -}; -export type CrossRepoEvaluationSummary = { - total: number; - passed: number; - failed: number; - majorityPassed: boolean; - withoutLoopoverConfig: number; - failuresByCategory: Record; -}; -type EvaluateRepoReadinessOptions = { - repoPath?: string; - resolveRepoPath?: (entry: { - repoFullName: string; - }) => string; - env?: NodeJS.ProcessEnv; - existsSync?: (path: string) => boolean; - detectRepoStack?: (repoPath: string) => RepoStackResult; - resolveMinerGoalSpec?: (repoPath: string) => { - present: boolean; - }; - buildCodingTaskSpec?: (input: Record) => { - ready: boolean; - verdict?: string; - instructions?: string; - acceptanceCriteriaPath?: string; - }; -}; -/** Canonical `owner/repo` with exactly one slash and safe segments; anything else → null. */ -export declare function normalizeCrossRepoFullName(value: unknown): string | null; -/** - * Tolerant JSON manifest parser (#4788). Malformed input degrades to an empty repo list with warnings rather than - * throwing, mirroring the fleet-run-manifest / miner-goal-spec convention. - */ -export declare function parseCrossRepoEvaluationManifest(content: string | null | undefined): ParsedCrossRepoEvaluationManifest; -/** - * Scan agent instructions for positive loopover/LoopOver assumptions (#4788). Lines that already tell the agent - * *not* to assume LoopOver conventions (the negative guidance from buildValidationGuidance) are skipped. - */ -export declare function scanPositiveLoopoverAssumptions(text: string): Array<{ - id: string; - line: string; -}>; -/** - * Evaluate one benchmark repo's miner readiness without running a live coding agent (#4788). - */ -export declare function evaluateRepoReadiness(entry: CrossRepoEvaluationManifestRepo, options?: EvaluateRepoReadinessOptions): CrossRepoEvaluationResult; -/** - * Run the harness across every repo in a parsed manifest (#4788). - */ -export declare function runCrossRepoEvaluation(parsed: ParsedCrossRepoEvaluationManifest, options?: { - repoFilter?: string; -} & EvaluateRepoReadinessOptions): CrossRepoEvaluationResult[]; -/** - * Reduce per-repo results to pass/fail counts and whether a strict majority passed (#4788). - */ -export declare function summarizeCrossRepoEvaluation(results: CrossRepoEvaluationResult[]): CrossRepoEvaluationSummary; -/** - * Human-readable pass/fail report for one evaluation run (#4788). - */ -export declare function formatCrossRepoEvaluationReport(results: CrossRepoEvaluationResult[], summary?: CrossRepoEvaluationSummary): string; -/** Execution-specific failure taxonomy (#7634), extending — not replacing — CROSS_REPO_FAILURE_CATEGORY. */ -export declare const CROSS_REPO_EXECUTION_FAILURE_CATEGORY: Readonly<{ - AGENT_RUN: "agent_run_failed"; - NOOP_DIFF: "noop_diff"; - BUILD: "build_failed"; - TEST: "test_failed"; -}>; -/** A benchmark attempt works a small synthetic issue, so a modest turn cap keeps dry-runs bounded without - * starving a real agent; callers tune via options.maxTurns. */ -export declare const DEFAULT_CROSS_REPO_EXECUTION_MAX_TURNS: number; -/** Per-command (build, then test) wall-clock cap — generous enough for a cold dependency install on the larger - * benchmark repos, small enough that a hung suite cannot wedge the whole run. */ -export declare const DEFAULT_CROSS_REPO_EXECUTION_COMMAND_TIMEOUT_MS: number; -export type CrossRepoExecutionCommandResult = { - code: number | null; - stdout: string; - stderr: string; - timedOut: boolean; -}; -export type CrossRepoExecutionRunCommandFn = (command: string, options: { - cwd: string; - timeoutMs: number; -}) => Promise; -export type CrossRepoExecutionWorkspace = { - path: string; - cleanup: () => void; -}; -/** Structural mirror of the engine's CodingAgentDriver contract — kept local so this module only loads the real - * driver construction (and its engine dependency) lazily, on the one path that actually runs an agent. */ -export type CrossRepoExecutionDriver = { - run(task: { - attemptId: string; - workingDirectory: string; - acceptanceCriteriaPath: string; - instructions: string; - maxTurns: number; - }): Promise<{ - ok: boolean; - changedFiles: readonly string[]; - summary: string; - error?: string | undefined; - }>; -}; -export type CrossRepoExecutionDetails = { - attempted: boolean; - changedFileCount: number | null; - buildRan: boolean; - testRan: boolean; -}; -export type CrossRepoExecutionEvaluationResult = CrossRepoEvaluationResult & { - execution: CrossRepoExecutionDetails | null; -}; -export type EvaluateRepoExecutionOptions = EvaluateRepoReadinessOptions & { - driver?: CrossRepoExecutionDriver; - prepareExecutionWorkspace?: (repoPath: string) => CrossRepoExecutionWorkspace; - runCommand?: CrossRepoExecutionRunCommandFn; - maxTurns?: number; - commandTimeoutMs?: number; -}; -/** Copy the benchmark clone into a discardable temp tree — the agent and the repo's test suite only ever touch - * the copy, so the clone stays pristine and cleanup is a single recursive remove. realpathSync's the fresh - * mkdtemp directory immediately: on macOS, os.tmpdir() resolves under a symlink (/var/folders/... -> - * /private/var/folders/...), so without this, the raw mkdtempSync path and coding-task-spec.ts's own - * realpathSync(workingDirectory) (a deliberate containment-check canonicalization in writeAcceptanceCriteriaFile, - * not something to remove) disagree on which string names the same directory -- acceptanceCriteriaPath then - * fails a plain acceptanceCriteriaPath.startsWith(workingDirectory) check even though the file genuinely is - * inside the working directory. Resolving once here, at the source, keeps every downstream path (workspace.path, - * task.workingDirectory, the acceptance-criteria path) in the same canonical form with no further changes - * needed. A no-op on Linux CI runners, where /tmp is not itself a symlink. */ -export declare function defaultPrepareExecutionWorkspace(repoPath: string): CrossRepoExecutionWorkspace; -/** Command runner for the stack's inferred build/test commands. detectRepoStack only ever emits simple - * `tool subcommand` forms ("npm test", "cargo build", "npm run build"), so the command is tokenized on - * whitespace and exec'd DIRECTLY -- deliberately no `shell: true`, so nothing in a benchmark repo's manifest - * can smuggle shell metacharacters into an interpreted shell line. Mirrors coding-agent-construction's - * createRealCliSubprocessSpawn otherwise: capture both streams and RESOLVE (never reject) on timeout or spawn - * error, so partial output stays diagnosable. Promise resolution is idempotent, so a `close` firing after the - * timeout already resolved needs no guard. */ -export declare function createDefaultCrossRepoExecutionRunCommand(): CrossRepoExecutionRunCommandFn; -/** - * Run the full discover -> plan -> code -> test loop for one benchmark repo, dry-run (#7634). Readiness gates - * first (its failures pass through unchanged); execution then happens entirely inside a scratch copy that is - * discarded in every outcome. - */ -export declare function evaluateRepoExecution(entry: CrossRepoEvaluationManifestRepo, options?: EvaluateRepoExecutionOptions): Promise; -/** - * Run full-execution mode across every repo in a parsed manifest (#7634), sequentially — agent runs and test - * suites are heavyweight, so no parallel fan-out. - */ -export declare function runCrossRepoFullExecution(parsed: ParsedCrossRepoEvaluationManifest, options?: { - repoFilter?: string; -} & EvaluateRepoExecutionOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/cross-repo-evaluation.js b/packages/loopover-miner/lib/cross-repo-evaluation.js deleted file mode 100644 index b7acd4bb75..0000000000 --- a/packages/loopover-miner/lib/cross-repo-evaluation.js +++ /dev/null @@ -1,569 +0,0 @@ -// Cross-repo evaluation harness (#4788): a repeatable, offline-first readiness check that asks whether the miner -// can approach a diverse benchmark repo set without loopover-specific target-repo configuration. Each repo is -// evaluated through the same stack-detection + coding-task-spec path a real attempt uses (detectRepoStack, -// resolveMinerGoalSpec, buildCodingTaskSpec) and failures are categorized as stack-detection gaps, execution -// readiness gaps, leaked loopover assumptions in agent instructions, clone/setup problems, or other. -import { spawn as nodeSpawn } from "node:child_process"; -import { cpSync, existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -// Already a transitive dependency via coding-task-spec.js's own engine imports -- this adds no load weight. -import { ACCEPTANCE_CRITERIA_FILENAME } from "@loopover/engine"; -import { buildCodingTaskSpec } from "./coding-task-spec.js"; -import { resolveMinerGoalSpec } from "./miner-goal-spec.js"; -import { isValidRepoSegment, resolveRepoCloneDir } from "./repo-clone.js"; -import { detectRepoStack } from "./stack-detection.js"; -/** Failure taxonomy surfaced in per-repo reports (#4788). */ -export const CROSS_REPO_FAILURE_CATEGORY = Object.freeze({ - STACK_DETECTION: "stack_detection_gap", - EXECUTION: "execution_gap", - GITTENSOR_ASSUMPTION: "loopover_assumption", - CLONE_SETUP: "clone_setup", - OTHER: "other", -}); -/** Instruction substrings that indicate a POSITIVE loopover/LoopOver CI assumption leaked into the agent prompt. - * Lines that explicitly tell the agent *not* to assume these are filtered out before scanning. */ -export const GITTENSOR_POSITIVE_ASSUMPTION_CHECKS = Object.freeze([ - { id: "test_ci_script", pattern: /npm run test:ci/i }, - { id: "codecov_patch", pattern: /codecov\/patch/i }, - { id: "gittensor_label", pattern: /gittensor:(?:bug|feature|priority)/i }, - { id: "loopover_gate", pattern: /loopover gate/i }, -]); -export const DEFAULT_CROSS_REPO_MANIFEST_RELATIVE_PATH = "benchmarks/cross-repo/manifest.json"; -export const MAX_CROSS_REPO_MANIFEST_BYTES = 65_536; -export const MAX_CROSS_REPO_MANIFEST_REPOS = 100; -// True UTF-8 byte count for the size guard (#7223): JS string `.length` is UTF-16 code units, which under-counts -// any multi-byte character (up to 4x for astral-plane code points), so `MAX_CROSS_REPO_MANIFEST_BYTES` -- named -// and warned about in BYTES -- was actually being compared against a code-unit count. Mirrors the identical helper -// in the three siblings this parser's own comment claims to follow: fleet-run-manifest.ts, miner-goal-spec.ts, -// and ams-policy-spec.ts. -function utf8ByteLength(value) { - let bytes = 0; - for (const char of value) { - const codePoint = char.codePointAt(0); - if (codePoint <= 0x7f) - bytes += 1; - else if (codePoint <= 0x7ff) - bytes += 2; - else if (codePoint <= 0xffff) - bytes += 3; - else - bytes += 4; - } - return bytes; -} -function cloneEmptyManifest(warnings = []) { - return { present: false, manifest: { repos: [] }, warnings }; -} -/** Canonical `owner/repo` with exactly one slash and safe segments; anything else → null. */ -export function normalizeCrossRepoFullName(value) { - if (typeof value !== "string") - return null; - const [owner, repo, extra] = value.trim().split("/"); - if (!owner || !repo || extra !== undefined) - return null; - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - return null; - return `${owner}/${repo}`; -} -function normalizeBoolean(value, field, fallback, warnings) { - if (value === undefined || value === null) - return fallback; - if (typeof value === "boolean") - return value; - warnings.push(`CrossRepoEvaluationManifest field "${field}" must be a boolean; falling back to ${fallback}.`); - return fallback; -} -function normalizeOptionalString(value, field, warnings) { - if (value === undefined || value === null) - return null; - if (typeof value !== "string") { - warnings.push(`CrossRepoEvaluationManifest field "${field}" must be a string; ignoring the value.`); - return null; - } - const trimmed = value.trim(); - return trimmed || null; -} -function normalizeRepoList(value, warnings) { - if (value === undefined || value === null) - return []; - if (!Array.isArray(value)) { - warnings.push(`CrossRepoEvaluationManifest field "repos" must be a list; ignoring a ${typeof value} value.`); - return []; - } - const result = []; - const seen = new Set(); - for (const [index, entry] of value.entries()) { - if (index >= MAX_CROSS_REPO_MANIFEST_REPOS) { - warnings.push(`CrossRepoEvaluationManifest field "repos" exceeded ${MAX_CROSS_REPO_MANIFEST_REPOS} entries; extra entries ignored.`); - break; - } - let repoFullName = null; - let stackHint = null; - let requireTestCommand = false; - let fixturePath = null; - if (typeof entry === "string") { - repoFullName = normalizeCrossRepoFullName(entry); - } - else if (entry && typeof entry === "object" && !Array.isArray(entry)) { - const record = entry; - repoFullName = normalizeCrossRepoFullName(record.repoFullName); - stackHint = normalizeOptionalString(record.stackHint, "stackHint", warnings); - requireTestCommand = normalizeBoolean(record.requireTestCommand, "requireTestCommand", false, warnings); - fixturePath = normalizeOptionalString(record.fixturePath, "fixturePath", warnings); - } - else { - warnings.push(`CrossRepoEvaluationManifest "repos" skipped a non-string, non-mapping entry.`); - continue; - } - if (repoFullName === null) { - warnings.push(`CrossRepoEvaluationManifest "repos" skipped an entry with an invalid "owner/repo" name.`); - continue; - } - if (seen.has(repoFullName)) { - warnings.push(`CrossRepoEvaluationManifest "repos" skipped a duplicate entry for ${repoFullName}.`); - continue; - } - seen.add(repoFullName); - const normalized = { repoFullName, requireTestCommand }; - if (stackHint) - normalized.stackHint = stackHint; - if (fixturePath) - normalized.fixturePath = fixturePath; - result.push(normalized); - } - return result; -} -/** - * Tolerant JSON manifest parser (#4788). Malformed input degrades to an empty repo list with warnings rather than - * throwing, mirroring the fleet-run-manifest / miner-goal-spec convention. - */ -export function parseCrossRepoEvaluationManifest(content) { - if (content === undefined || content === null) - return cloneEmptyManifest(); - if (typeof content !== "string") { - return cloneEmptyManifest([`CrossRepoEvaluationManifest content must be a string; got ${typeof content}.`]); - } - const trimmed = content.trim(); - if (!trimmed) - return cloneEmptyManifest(); - if (utf8ByteLength(trimmed) > MAX_CROSS_REPO_MANIFEST_BYTES) { - return cloneEmptyManifest([ - `CrossRepoEvaluationManifest exceeded ${MAX_CROSS_REPO_MANIFEST_BYTES} bytes; ignoring the file.`, - ]); - } - let raw; - try { - raw = JSON.parse(trimmed); - } - catch { - return cloneEmptyManifest(["CrossRepoEvaluationManifest is not valid JSON."]); - } - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - return cloneEmptyManifest(["CrossRepoEvaluationManifest root must be a JSON object."]); - } - const warnings = []; - const repos = normalizeRepoList(raw.repos, warnings); - return { present: true, manifest: { repos }, warnings }; -} -/** - * Scan agent instructions for positive loopover/LoopOver assumptions (#4788). Lines that already tell the agent - * *not* to assume LoopOver conventions (the negative guidance from buildValidationGuidance) are skipped. - */ -export function scanPositiveLoopoverAssumptions(text) { - if (typeof text !== "string") - return []; - const findings = []; - for (const line of text.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || /do not assume/i.test(trimmed)) - continue; - for (const check of GITTENSOR_POSITIVE_ASSUMPTION_CHECKS) { - if (check.pattern.test(line)) - findings.push({ id: check.id, line: trimmed }); - } - } - return findings; -} -/** One shared thrown-value formatter so every catch in both harness modes carries the same two branches. */ -function describeError(error) { - return error instanceof Error ? error.message : String(error); -} -function buildFailure(repoFullName, category, reason, extra = {}) { - return { - repoFullName, - passed: false, - failureCategory: category, - reason, - stackDetected: false, - usedDefaultGoalSpec: null, - assumptionFindings: [], - ...extra, - }; -} -function buildPass(repoFullName, extra = {}) { - return { - repoFullName, - passed: true, - failureCategory: null, - reason: null, - stackDetected: true, - usedDefaultGoalSpec: true, - assumptionFindings: [], - ...extra, - }; -} -function resolveEvaluationRepoPath(entry, options = {}) { - if (entry.fixturePath && typeof entry.fixturePath === "string") - return entry.fixturePath; - if (typeof options.repoPath === "string" && options.repoPath.trim()) - return options.repoPath.trim(); - if (typeof options.resolveRepoPath === "function") - return options.resolveRepoPath(entry); - return resolveRepoCloneDir(entry.repoFullName, options.env ?? process.env); -} -function defaultClaimLedger(repoFullName) { - return { listClaims: () => [] }; -} -/** The synthetic-issue spec input both harness modes hand to buildCodingTaskSpec — readiness composes it against - * the benchmark clone, full-execution mode against the scratch workspace copy (#7634), so the acceptance-criteria - * file buildCodingTaskSpec writes lands inside whichever tree the caller is actually working in. */ -function buildHarnessSpecInput(repoFullName, workingDirectory, detectImpl) { - return { - repoFullName, - issue: { - number: 1, - title: "Cross-repo evaluation harness smoke issue", - body: "Synthetic issue used only by the cross-repo evaluation harness.", - labels: ["bug"], - }, - context: { issues: [{ number: 1 }], pullRequests: [] }, - claimLedger: defaultClaimLedger(repoFullName), - workingDirectory, - detectRepoStack: detectImpl, - }; -} -/** - * Evaluate one benchmark repo's miner readiness without running a live coding agent (#4788). - */ -export function evaluateRepoReadiness(entry, options = {}) { - const repoFullName = entry?.repoFullName; - if (typeof repoFullName !== "string" || !normalizeCrossRepoFullName(repoFullName)) { - return buildFailure(typeof repoFullName === "string" ? repoFullName : "(invalid)", CROSS_REPO_FAILURE_CATEGORY.OTHER, "Benchmark entry is missing a valid owner/repo name."); - } - const existsImpl = options.existsSync ?? existsSync; - const detectImpl = options.detectRepoStack ?? detectRepoStack; - const goalSpecImpl = options.resolveMinerGoalSpec ?? resolveMinerGoalSpec; - const buildSpecImpl = options.buildCodingTaskSpec ?? - buildCodingTaskSpec; - const repoPath = resolveEvaluationRepoPath(entry, options); - if (!existsImpl(repoPath)) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.CLONE_SETUP, `Repository path does not exist: ${repoPath}. Clone the repo or set LOOPOVER_MINER_REPO_CLONE_DIR.`); - } - const goalSpec = goalSpecImpl(repoPath); - const usedDefaultGoalSpec = goalSpec?.present !== true; - const stack = detectImpl(repoPath); - if (stack?.detected !== true) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.STACK_DETECTION, stack?.reason ?? "Stack auto-detection did not recognize this repository.", { stackDetected: false, usedDefaultGoalSpec }); - } - if (entry.requireTestCommand === true && !stack.testCommand) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.EXECUTION, "Stack detection succeeded but no test command was inferred while requireTestCommand is set.", { stackDetected: true, usedDefaultGoalSpec, stack }); - } - let specResult; - try { - specResult = buildSpecImpl(buildHarnessSpecInput(repoFullName, repoPath, detectImpl)); - } - catch (error) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.OTHER, describeError(error), { - stackDetected: true, - usedDefaultGoalSpec, - stack, - }); - } - if (specResult?.ready !== true) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.EXECUTION, `Coding task spec is not ready (verdict: ${specResult?.verdict ?? "unknown"}).`, { stackDetected: true, usedDefaultGoalSpec, stack }); - } - const assumptionFindings = scanPositiveLoopoverAssumptions(specResult.instructions ?? ""); - if (assumptionFindings.length > 0) { - return buildFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.GITTENSOR_ASSUMPTION, `Agent instructions leak loopover-specific assumptions (${assumptionFindings.map((f) => f.id).join(", ")}).`, { stackDetected: true, usedDefaultGoalSpec, stack, assumptionFindings }); - } - return buildPass(repoFullName, { usedDefaultGoalSpec, stack }); -} -/** - * Run the harness across every repo in a parsed manifest (#4788). - */ -export function runCrossRepoEvaluation(parsed, options = {}) { - const repos = parsed?.manifest?.repos ?? []; - const results = []; - for (const entry of repos) { - if (options.repoFilter && entry.repoFullName !== options.repoFilter) - continue; - results.push(evaluateRepoReadiness(entry, options)); - } - return results; -} -/** - * Reduce per-repo results to pass/fail counts and whether a strict majority passed (#4788). - */ -export function summarizeCrossRepoEvaluation(results) { - const list = Array.isArray(results) ? results : []; - let passed = 0; - let failed = 0; - const failuresByCategory = {}; - for (const result of list) { - if (result?.passed === true) { - passed += 1; - continue; - } - failed += 1; - const category = result?.failureCategory ?? CROSS_REPO_FAILURE_CATEGORY.OTHER; - failuresByCategory[category] = (failuresByCategory[category] ?? 0) + 1; - } - const total = passed + failed; - const majorityPassed = total > 0 ? passed > failed : false; - const withoutLoopoverConfig = list.filter((r) => r?.usedDefaultGoalSpec !== false).length; - return { - total, - passed, - failed, - majorityPassed, - withoutLoopoverConfig, - failuresByCategory, - }; -} -/** - * Human-readable pass/fail report for one evaluation run (#4788). - */ -export function formatCrossRepoEvaluationReport(results, summary = summarizeCrossRepoEvaluation(results)) { - const lines = ["loopover-miner cross-repo evaluation", ""]; - for (const result of results) { - if (result.passed) { - lines.push(`PASS ${result.repoFullName}`); - continue; - } - lines.push(`FAIL ${result.repoFullName} [${result.failureCategory}] ${result.reason}`); - } - lines.push("", `summary: ${summary.passed}/${summary.total} passed` + - (summary.majorityPassed ? " (majority passed)" : " (majority failed)")); - if (summary.total > 0) { - lines.push(`without loopover-specific target config: ${summary.withoutLoopoverConfig}/${summary.total}`); - } - const categories = Object.entries(summary.failuresByCategory).sort(([a], [b]) => a.localeCompare(b)); - if (categories.length > 0) { - lines.push("", "failures by category:"); - for (const [category, count] of categories) { - lines.push(`- ${category}: ${count}`); - } - } - return lines.join("\n"); -} -// --------------------------------------------------------------------------------------------------------------- -// Full-execution mode (#7634): past readiness, actually run the discover -> plan -> code -> test loop against a -// benchmark repo — dry-run only. The attempt runs inside a scratch COPY of the local clone that is discarded -// afterward: no live PR submission, no forge API calls, and the benchmark clone itself is never mutated by the -// agent or the test run. Same taxonomy extension point as the readiness categories above — summarize/format are -// data-driven over category strings, so these flow through the existing report unchanged. -/** Execution-specific failure taxonomy (#7634), extending — not replacing — CROSS_REPO_FAILURE_CATEGORY. */ -export const CROSS_REPO_EXECUTION_FAILURE_CATEGORY = Object.freeze({ - AGENT_RUN: "agent_run_failed", - NOOP_DIFF: "noop_diff", - BUILD: "build_failed", - TEST: "test_failed", -}); -/** A benchmark attempt works a small synthetic issue, so a modest turn cap keeps dry-runs bounded without - * starving a real agent; callers tune via options.maxTurns. */ -export const DEFAULT_CROSS_REPO_EXECUTION_MAX_TURNS = 24; -/** Per-command (build, then test) wall-clock cap — generous enough for a cold dependency install on the larger - * benchmark repos, small enough that a hung suite cannot wedge the whole run. */ -export const DEFAULT_CROSS_REPO_EXECUTION_COMMAND_TIMEOUT_MS = 600_000; -/** Copy the benchmark clone into a discardable temp tree — the agent and the repo's test suite only ever touch - * the copy, so the clone stays pristine and cleanup is a single recursive remove. realpathSync's the fresh - * mkdtemp directory immediately: on macOS, os.tmpdir() resolves under a symlink (/var/folders/... -> - * /private/var/folders/...), so without this, the raw mkdtempSync path and coding-task-spec.ts's own - * realpathSync(workingDirectory) (a deliberate containment-check canonicalization in writeAcceptanceCriteriaFile, - * not something to remove) disagree on which string names the same directory -- acceptanceCriteriaPath then - * fails a plain acceptanceCriteriaPath.startsWith(workingDirectory) check even though the file genuinely is - * inside the working directory. Resolving once here, at the source, keeps every downstream path (workspace.path, - * task.workingDirectory, the acceptance-criteria path) in the same canonical form with no further changes - * needed. A no-op on Linux CI runners, where /tmp is not itself a symlink. */ -export function defaultPrepareExecutionWorkspace(repoPath) { - const scratchRoot = realpathSync(mkdtempSync(join(tmpdir(), "loopover-cross-repo-exec-"))); - const path = join(scratchRoot, "repo"); - cpSync(repoPath, path, { recursive: true }); - return { - path, - cleanup: () => { - rmSync(scratchRoot, { recursive: true, force: true }); - }, - }; -} -/** Command runner for the stack's inferred build/test commands. detectRepoStack only ever emits simple - * `tool subcommand` forms ("npm test", "cargo build", "npm run build"), so the command is tokenized on - * whitespace and exec'd DIRECTLY -- deliberately no `shell: true`, so nothing in a benchmark repo's manifest - * can smuggle shell metacharacters into an interpreted shell line. Mirrors coding-agent-construction's - * createRealCliSubprocessSpawn otherwise: capture both streams and RESOLVE (never reject) on timeout or spawn - * error, so partial output stays diagnosable. Promise resolution is idempotent, so a `close` firing after the - * timeout already resolved needs no guard. */ -export function createDefaultCrossRepoExecutionRunCommand() { - return (command, options) => new Promise((resolve) => { - const [executable, ...args] = command.split(/\s+/).filter(Boolean); - if (!executable) { - resolve({ stdout: "", stderr: "empty_command", code: null, timedOut: false }); - return; - } - const child = nodeSpawn(executable, args, { - cwd: options.cwd, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - const timer = setTimeout(() => { - child.kill("SIGKILL"); - resolve({ stdout, stderr, code: null, timedOut: true }); - }, options.timeoutMs); - // stdio is always ["ignore","pipe","pipe"] above, so both streams exist — assert instead of branching. - child.stdout.on("data", (chunk) => { - stdout += chunk.toString("utf8"); - }); - child.stderr.on("data", (chunk) => { - stderr += chunk.toString("utf8"); - }); - child.on("error", (err) => { - clearTimeout(timer); - resolve({ stdout, stderr: err.message, code: null, timedOut: false }); - }); - child.on("close", (code) => { - clearTimeout(timer); - resolve({ stdout, stderr, code, timedOut: false }); - }); - }); -} -function buildExecutionFailure(repoFullName, category, reason, base, execution) { - return { ...buildFailure(repoFullName, category, reason, base), execution }; -} -/** - * Run the full discover -> plan -> code -> test loop for one benchmark repo, dry-run (#7634). Readiness gates - * first (its failures pass through unchanged); execution then happens entirely inside a scratch copy that is - * discarded in every outcome. - */ -export async function evaluateRepoExecution(entry, options = {}) { - const readiness = evaluateRepoReadiness(entry, options); - if (!readiness.passed) - return { ...readiness, execution: null }; - const prepareWorkspace = options.prepareExecutionWorkspace ?? defaultPrepareExecutionWorkspace; - let workspace; - try { - workspace = prepareWorkspace(resolveEvaluationRepoPath(entry, options)); - } - catch (error) { - const reason = `Failed to prepare a scratch execution workspace: ${describeError(error)}`; - // A passed readiness result always carries its detected stack (buildPass is invoked with it). - const base = { stackDetected: true, usedDefaultGoalSpec: readiness.usedDefaultGoalSpec, stack: readiness.stack }; - return buildExecutionFailure(readiness.repoFullName, CROSS_REPO_FAILURE_CATEGORY.CLONE_SETUP, reason, base, null); - } - try { - return await runExecutionPhases(readiness, workspace, options); - } - finally { - try { - workspace.cleanup(); - } - catch { - // Best-effort discard — a cleanup failure must never mask the evaluation outcome itself. - } - } -} -async function runExecutionPhases(readiness, workspace, options) { - const repoFullName = readiness.repoFullName; - // Readiness passed, so the stack is present and detected — narrow once instead of re-branching on it. - const stack = readiness.stack; - const usedDefaultGoalSpec = readiness.usedDefaultGoalSpec; - const failureBase = { stackDetected: true, usedDefaultGoalSpec, stack }; - const detectImpl = options.detectRepoStack ?? detectRepoStack; - const buildSpecImpl = options.buildCodingTaskSpec ?? - buildCodingTaskSpec; - // Re-compose the coding-task spec INSIDE the scratch copy: buildCodingTaskSpec writes the acceptance-criteria - // document into its workingDirectory, and the agent must find it (and work) in the tree it is allowed to touch. - // The readiness pass already wrote that document into the clone (pre-existing #4788 behavior), so the copy - // inherits it -- and writeAcceptanceCriteriaFile opens O_EXCL (never overwrites), so clear the inherited file - // first. The scratch tree is ours to mutate; the clone itself stays untouched. - rmSync(join(workspace.path, ACCEPTANCE_CRITERIA_FILENAME), { force: true }); - let specResult; - try { - specResult = buildSpecImpl(buildHarnessSpecInput(repoFullName, workspace.path, detectImpl)); - } - catch (error) { - return buildExecutionFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.OTHER, `Coding task spec failed inside the scratch workspace: ${describeError(error)}`, failureBase, null); - } - if (specResult?.ready !== true) { - return buildExecutionFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.EXECUTION, `Coding task spec is not ready inside the scratch workspace (verdict: ${specResult?.verdict ?? "unknown"}).`, failureBase, null); - } - let driver = options.driver; - if (!driver) { - try { - // Lazy so the readiness-only path (and its consumers) never load the engine-backed driver construction. - const { constructProductionCodingAgentDriver } = await import("./coding-agent-construction.js"); - driver = constructProductionCodingAgentDriver(options.env ?? process.env); - } - catch (error) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.AGENT_RUN, `No runnable coding-agent driver: ${describeError(error)}`, failureBase, { attempted: false, changedFileCount: null, buildRan: false, testRan: false }); - } - } - const task = { - attemptId: `cross-repo-eval-${repoFullName.replace("/", "-")}`, - workingDirectory: workspace.path, - acceptanceCriteriaPath: specResult.acceptanceCriteriaPath ?? join(workspace.path, "ACCEPTANCE_CRITERIA.md"), - instructions: specResult.instructions ?? "", - maxTurns: options.maxTurns ?? DEFAULT_CROSS_REPO_EXECUTION_MAX_TURNS, - }; - let agentResult; - try { - agentResult = await driver.run(task); - } - catch (error) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.AGENT_RUN, `Coding agent run threw: ${describeError(error)}`, failureBase, { attempted: true, changedFileCount: null, buildRan: false, testRan: false }); - } - if (agentResult?.ok !== true) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.AGENT_RUN, `Coding agent run did not succeed: ${agentResult?.error ?? agentResult?.summary ?? "no failure detail reported"}`, failureBase, { attempted: true, changedFileCount: null, buildRan: false, testRan: false }); - } - const changedFileCount = Array.isArray(agentResult.changedFiles) ? agentResult.changedFiles.length : 0; - if (changedFileCount === 0) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.NOOP_DIFF, "Coding agent reported success but the generated diff is a no-op (no files changed).", failureBase, { attempted: true, changedFileCount: 0, buildRan: false, testRan: false }); - } - const runCommand = options.runCommand ?? createDefaultCrossRepoExecutionRunCommand(); - const commandTimeoutMs = options.commandTimeoutMs ?? DEFAULT_CROSS_REPO_EXECUTION_COMMAND_TIMEOUT_MS; - let buildRan = false; - if (stack.buildCommand) { - buildRan = true; - const buildResult = await runCommand(stack.buildCommand, { cwd: workspace.path, timeoutMs: commandTimeoutMs }); - if (buildResult.timedOut || buildResult.code !== 0) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.BUILD, `Diff generated but the build ${buildResult.timedOut ? "timed out" : `failed (exit ${buildResult.code})`}: ${stack.buildCommand}`, failureBase, { attempted: true, changedFileCount, buildRan: true, testRan: false }); - } - } - if (!stack.testCommand) { - return buildExecutionFailure(repoFullName, CROSS_REPO_FAILURE_CATEGORY.EXECUTION, "Stack detection inferred no test command, so the generated diff cannot be validated in execution mode.", failureBase, { attempted: true, changedFileCount, buildRan, testRan: false }); - } - const testResult = await runCommand(stack.testCommand, { cwd: workspace.path, timeoutMs: commandTimeoutMs }); - if (testResult.timedOut || testResult.code !== 0) { - return buildExecutionFailure(repoFullName, CROSS_REPO_EXECUTION_FAILURE_CATEGORY.TEST, `Diff generated but the repo's own test suite ${testResult.timedOut ? "timed out" : `failed (exit ${testResult.code})`}: ${stack.testCommand}`, failureBase, { attempted: true, changedFileCount, buildRan, testRan: true }); - } - return { - ...buildPass(repoFullName, { usedDefaultGoalSpec, stack }), - execution: { attempted: true, changedFileCount, buildRan, testRan: true }, - }; -} -/** - * Run full-execution mode across every repo in a parsed manifest (#7634), sequentially — agent runs and test - * suites are heavyweight, so no parallel fan-out. - */ -export async function runCrossRepoFullExecution(parsed, options = {}) { - const repos = parsed?.manifest?.repos ?? []; - const results = []; - for (const entry of repos) { - if (options.repoFilter && entry.repoFullName !== options.repoFilter) - continue; - results.push(await evaluateRepoExecution(entry, options)); - } - return results; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3Jvc3MtcmVwby1ldmFsdWF0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY3Jvc3MtcmVwby1ldmFsdWF0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGlIQUFpSDtBQUNqSCw4R0FBOEc7QUFDOUcsMkdBQTJHO0FBQzNHLDZHQUE2RztBQUM3RyxxR0FBcUc7QUFFckcsT0FBTyxFQUFFLEtBQUssSUFBSSxTQUFTLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUN4RCxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNoRixPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDakMsNEdBQTRHO0FBQzVHLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQzVELE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzVELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUd2RCw2REFBNkQ7QUFDN0QsTUFBTSxDQUFDLE1BQU0sMkJBQTJCLEdBTW5DLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDakIsZUFBZSxFQUFFLHFCQUFxQjtJQUN0QyxTQUFTLEVBQUUsZUFBZTtJQUMxQixvQkFBb0IsRUFBRSxxQkFBcUI7SUFDM0MsV0FBVyxFQUFFLGFBQWE7SUFDMUIsS0FBSyxFQUFFLE9BQU87Q0FDZixDQUFDLENBQUM7QUFFSDttR0FDbUc7QUFDbkcsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQW1ELE1BQU0sQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/deny-check.d.ts b/packages/loopover-miner/lib/deny-check.d.ts deleted file mode 100644 index e67ffed277..0000000000 --- a/packages/loopover-miner/lib/deny-check.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type ParsedDenyCheckArgs = { - tool: string; - input: Record; - json: boolean; -} | { - error: string; -}; -export declare function parseDenyCheckArgs(args: string[]): ParsedDenyCheckArgs; -export declare function runDenyCheck(args: string[]): number; diff --git a/packages/loopover-miner/lib/deny-check.js b/packages/loopover-miner/lib/deny-check.js deleted file mode 100644 index 02cca5b5b0..0000000000 --- a/packages/loopover-miner/lib/deny-check.js +++ /dev/null @@ -1,77 +0,0 @@ -import { evaluateDenyHooks } from "./deny-hooks.js"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -const DENY_CHECK_USAGE = "Usage: loopover-miner hooks check --tool --input [--json]"; -function parseToolInput(raw) { - if (raw === undefined) { - return { error: "Missing value for --input." }; - } - try { - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { error: "Tool input must be a JSON object." }; - } - return { value: parsed }; - } - catch { - return { error: "Tool input must be valid JSON." }; - } -} -export function parseDenyCheckArgs(args) { - const options = { - json: false, - tool: undefined, - input: undefined, - }; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === undefined) - continue; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--tool" || token === "--name") { - const tool = args[++index]; - if (!tool || tool.startsWith("-")) - return { error: "Missing value for --tool." }; - options.tool = tool; - continue; - } - if (token === "--input") { - const raw = args[++index]; - if (!raw || raw.startsWith("-")) - return { error: "Missing value for --input." }; - const parsed = parseToolInput(raw); - if ("error" in parsed) - return { error: parsed.error }; - options.input = parsed.value; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - return { error: DENY_CHECK_USAGE }; - } - if (!options.tool || !options.input) { - return { error: DENY_CHECK_USAGE }; - } - return { tool: options.tool, input: options.input, json: options.json }; -} -export function runDenyCheck(args) { - const parsed = parseDenyCheckArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const verdict = evaluateDenyHooks({ name: parsed.tool, input: parsed.input }); - if (parsed.json) { - console.log(JSON.stringify(verdict)); - } - else if (!verdict.allowed) { - console.error(verdict.blockedBy?.reason ?? "Blocked by deny hook."); - } - else { - console.log("allowed"); - } - return verdict.allowed ? 0 : 1; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVueS1jaGVjay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRlbnktY2hlY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDcEQsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWhFLE1BQU0sZ0JBQWdCLEdBQ3BCLHlFQUF5RSxDQUFDO0FBVTVFLFNBQVMsY0FBYyxDQUFDLEdBQXVCO0lBQzdDLElBQUksR0FBRyxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQ3RCLE9BQU8sRUFBRSxLQUFLLEVBQUUsNEJBQTRCLEVBQUUsQ0FBQztJQUNqRCxDQUFDO0lBQ0QsSUFBSSxDQUFDO1FBQ0gsTUFBTSxNQUFNLEdBQVksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUN4QyxJQUFJLENBQUMsTUFBTSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7WUFDbkUsT0FBTyxFQUFFLEtBQUssRUFBRSxtQ0FBbUMsRUFBRSxDQUFDO1FBQ3hELENBQUM7UUFDRCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQWlDLEVBQUUsQ0FBQztJQUN0RCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxFQUFFLEtBQUssRUFBRSxnQ0FBZ0MsRUFBRSxDQUFDO0lBQ3JELENBQUM7QUFDSCxDQUFDO0FBRUQsTUFBTSxVQUFVLGtCQUFrQixDQUFDLElBQWM7SUFDL0MsTUFBTSxPQUFPLEdBQTRGO1FBQ3ZHLElBQUksRUFBRSxLQUFLO1FBQ1gsSUFBSSxFQUFFLFNBQVM7UUFDZixLQUFLLEVBQUUsU0FBUztLQUNqQixDQUFDO0lBRUYsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxJQUFJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/deny-hook-synthesis.d.ts b/packages/loopover-miner/lib/deny-hook-synthesis.d.ts deleted file mode 100644 index 66c0f0d6fd..0000000000 --- a/packages/loopover-miner/lib/deny-hook-synthesis.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { aggregateBlockerHistory, canonicalizeChangedPath, changedPathToDenyGlob, DEFAULT_SYNTHESIS_CONFIG, isCoveredByDefaultDenyRules, normalizeBlockerHistory, normalizeBlockerHistoryRecord, resolveEffectiveDenyRules, setProposalStatuses } from "@loopover/engine"; -import type { DenyRuleProposal, SynthesisConfig } from "@loopover/engine"; -import type { DenyRule } from "./deny-hooks.js"; -export { aggregateBlockerHistory, canonicalizeChangedPath, changedPathToDenyGlob, DEFAULT_SYNTHESIS_CONFIG, isCoveredByDefaultDenyRules, normalizeBlockerHistory, normalizeBlockerHistoryRecord, resolveEffectiveDenyRules, setProposalStatuses, }; -export type DenyHookSynthesisStore = { - dbPath: string; - refreshProposals(repoFullName: string, history: unknown, config?: SynthesisConfig, apiBaseUrl?: string): DenyRuleProposal[]; - listProposals(repoFullName: string, apiBaseUrl?: string): DenyRuleProposal[]; - setProposalStatus(repoFullName: string, proposalId: string, status: string, apiBaseUrl?: string): void; - resolveEffectiveRules(repoFullName: string, options?: { - includeDefaults?: boolean; - apiBaseUrl?: string; - }): DenyRule[]; - close(): void; -}; -/** - * Derive candidate deny-hook rules from blocker/path history. Miner-facing wrapper over the engine's pure - * `synthesizeDenyRuleProposals`, defaulting the injected clock to `Date.now()` so this keeps the pre-#5667 2-arg - * signature (and wall-clock `audit.synthesizedAt`) every existing caller and test relies on. Returns proposal - * objects only — nothing is active until a maintainer approves them (see resolveEffectiveDenyRules). - */ -export declare function synthesizeDenyRuleProposals(records: unknown, config?: SynthesisConfig): DenyRuleProposal[]; -export declare function resolveDenyHookSynthesisDbPath(env?: Record): string; -/** - * Local SQLite store for synthesized deny-rule proposals. Refresh re-derives proposals from history while - * preserving maintainer decisions on ids that still exist. - */ -export declare function initDenyHookSynthesisStore(dbPath?: string): DenyHookSynthesisStore; diff --git a/packages/loopover-miner/lib/deny-hook-synthesis.js b/packages/loopover-miner/lib/deny-hook-synthesis.js deleted file mode 100644 index ec15be0dbe..0000000000 --- a/packages/loopover-miner/lib/deny-hook-synthesis.js +++ /dev/null @@ -1,192 +0,0 @@ -// Synthesize PreToolUse deny-hook rule proposals from per-repo blocker/path history (#4522). The pure synthesis -// logic moved into `@loopover/engine` (packages/loopover-engine/src/miner/deny-hook-synthesis.ts) by #5667; -// this module is now a thin wrapper that re-exports those pure helpers and keeps the local SQLite store for -// refresh + maintainer review before any synthesized rule takes effect. Approved rules merge with -// {@link DEFAULT_DENY_RULES}; unapproved proposals never block tool calls. No behavior change. -import { chmodSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import { aggregateBlockerHistory, canonicalizeChangedPath, changedPathToDenyGlob, DEFAULT_SYNTHESIS_CONFIG, isCoveredByDefaultDenyRules, normalizeBlockerHistory, normalizeBlockerHistoryRecord, normalizeRepoFullName, proposalStatusSet, resolveEffectiveDenyRules, setProposalStatuses, synthesizeDenyRuleProposals as engineSynthesizeDenyRuleProposals, } from "@loopover/engine"; -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -// Re-export the pure synthesis helpers from the engine so this module's public API is unchanged after #5667 -// moved derivation/audit into @loopover/engine. Only the SQLite store below (and its forge/db-path helpers) is -// miner-local, because it depends on node:sqlite/node:fs and this package's forge-config default. -export { aggregateBlockerHistory, canonicalizeChangedPath, changedPathToDenyGlob, DEFAULT_SYNTHESIS_CONFIG, isCoveredByDefaultDenyRules, normalizeBlockerHistory, normalizeBlockerHistoryRecord, resolveEffectiveDenyRules, setProposalStatuses, }; -const defaultDbFileName = "deny-hook-synthesis.sqlite3"; -/** - * Derive candidate deny-hook rules from blocker/path history. Miner-facing wrapper over the engine's pure - * `synthesizeDenyRuleProposals`, defaulting the injected clock to `Date.now()` so this keeps the pre-#5667 2-arg - * signature (and wall-clock `audit.synthesizedAt`) every existing caller and test relies on. Returns proposal - * objects only — nothing is active until a maintainer approves them (see resolveEffectiveDenyRules). - */ -export function synthesizeDenyRuleProposals(records, config = {}) { - return engineSynthesizeDenyRuleProposals(records, config, Date.now()); -} -/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563). - * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */ -function normalizeApiBaseUrl(apiBaseUrl) { - if (apiBaseUrl === undefined || apiBaseUrl === null) - return DEFAULT_FORGE_CONFIG.apiBaseUrl; - if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) - throw new Error("invalid_api_base_url"); - return apiBaseUrl.trim(); -} -export function resolveDenyHookSynthesisDbPath(env = process.env) { - const explicitPath = typeof env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB === "string" - ? env.LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB.trim() - : ""; - if (explicitPath) - return explicitPath; - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" - ? env.LOOPOVER_MINER_CONFIG_DIR.trim() - : ""; - if (explicitConfigDir) - return join(explicitConfigDir, defaultDbFileName); - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultDbFileName); -} -// `dbPath` is always a real string here: this function's only caller (initDenyHookSynthesisStore) already -// defaults its own parameter to resolveDenyHookSynthesisDbPath() before ever reaching this call, so the -// nullish fallback historically here could never actually fire. -function normalizeDbPath(dbPath) { - const path = dbPath.trim(); - if (!path) - throw new Error("invalid_deny_hook_synthesis_db_path"); - return path; -} -function rowToProposal(row) { - return { - id: row.id, - status: row.status, - rule: JSON.parse(row.rule_json), - audit: JSON.parse(row.audit_json), - }; -} -// Rebuild deny_rule_proposals' (repo_full_name, id) PRIMARY KEY into a (api_base_url, repo_full_name, id) -// composite (#5563) -- two forge hosts serving a same-named owner/repo must not share one proposal row. SQLite -// cannot ALTER a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy every existing row -// with the pre-#4784 implicit single-forge default backfilled, drop the old table, rename the new one in. -// Guarded by a column-presence check (this module has no schema-version framework of its own, unlike the -// package's other local stores) so this only runs once per file. -function ensureDenyRuleProposalsForgeScope(db) { - const hasApiBaseUrlColumn = db - .prepare("PRAGMA table_info(deny_rule_proposals)") - .all() - .some((column) => column.name === "api_base_url"); - if (hasApiBaseUrlColumn) - return; - db.exec(` - CREATE TABLE deny_rule_proposals_v2 ( - api_base_url TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - id TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('proposed', 'approved', 'rejected')), - rule_json TEXT NOT NULL, - audit_json TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY (api_base_url, repo_full_name, id) - ) - `); - // OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `status`, - // e.g. from a hand-edited or otherwise corrupted file) would violate the CHECK constraint above and abort the - // whole migration. Skipping it here is consistent with that same fail-closed posture, rather than turning one - // bad row into a permanently unmigratable file. - db.prepare(`INSERT OR IGNORE INTO deny_rule_proposals_v2 (api_base_url, repo_full_name, id, status, rule_json, audit_json, updated_at) - SELECT ?, repo_full_name, id, status, rule_json, audit_json, updated_at FROM deny_rule_proposals`).run(DEFAULT_FORGE_CONFIG.apiBaseUrl); - db.exec("DROP TABLE deny_rule_proposals"); - db.exec("ALTER TABLE deny_rule_proposals_v2 RENAME TO deny_rule_proposals"); -} -/** - * Local SQLite store for synthesized deny-rule proposals. Refresh re-derives proposals from history while - * preserving maintainer decisions on ids that still exist. - */ -export function initDenyHookSynthesisStore(dbPath = resolveDenyHookSynthesisDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - mkdirSync(dirname(resolvedPath), { recursive: true, mode: 0o700 }); - const db = new DatabaseSync(resolvedPath); - chmodSync(resolvedPath, 0o600); - db.exec("PRAGMA busy_timeout = 5000"); - db.exec(` - CREATE TABLE IF NOT EXISTS deny_rule_proposals ( - repo_full_name TEXT NOT NULL, - id TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('proposed', 'approved', 'rejected')), - rule_json TEXT NOT NULL, - audit_json TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY (repo_full_name, id) - ) - `); - ensureDenyRuleProposalsForgeScope(db); - const upsertStatement = db.prepare(` - INSERT INTO deny_rule_proposals (api_base_url, repo_full_name, id, status, rule_json, audit_json, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(api_base_url, repo_full_name, id) DO UPDATE SET - status = excluded.status, - rule_json = excluded.rule_json, - audit_json = excluded.audit_json, - updated_at = excluded.updated_at - `); - const getStatusStatement = db.prepare("SELECT status FROM deny_rule_proposals WHERE api_base_url = ? AND repo_full_name = ? AND id = ?"); - const listStatement = db.prepare("SELECT repo_full_name, id, status, rule_json, audit_json, updated_at FROM deny_rule_proposals WHERE api_base_url = ? AND repo_full_name = ? ORDER BY id ASC"); - const setStatusStatement = db.prepare(` - UPDATE deny_rule_proposals SET status = ?, updated_at = ? WHERE api_base_url = ? AND repo_full_name = ? AND id = ? - `); - const store = { - dbPath: resolvedPath, - refreshProposals(repoFullName, history, config = {}, apiBaseUrl) { - const forge = normalizeApiBaseUrl(apiBaseUrl); - const repo = normalizeRepoFullName(repoFullName); - const synthesized = synthesizeDenyRuleProposals(history, config); - const updatedAt = new Date().toISOString(); - db.exec("BEGIN IMMEDIATE"); - try { - for (const proposal of synthesized) { - const existing = getStatusStatement.get(forge, repo, proposal.id); - const status = existing?.status && proposalStatusSet.has(existing.status) && existing.status !== "proposed" - ? existing.status - : "proposed"; - upsertStatement.run(forge, repo, proposal.id, status, JSON.stringify(proposal.rule), JSON.stringify(proposal.audit), updatedAt); - } - db.exec("COMMIT"); - } - catch (error) { - // Defensive: a genuine mid-transaction SQLite failure (disk full, corruption) rather than dead code -- - // deliberately not exercised by a contrived unit test, since forcing it would require mocking - // node:sqlite's DatabaseSync rather than driving this through the real public API. - db.exec("ROLLBACK"); - throw error; - } - return listStatement.all(forge, repo).map(rowToProposal); - }, - listProposals(repoFullName, apiBaseUrl) { - const forge = normalizeApiBaseUrl(apiBaseUrl); - const repo = normalizeRepoFullName(repoFullName); - return listStatement.all(forge, repo).map(rowToProposal); - }, - setProposalStatus(repoFullName, proposalId, status, apiBaseUrl) { - const forge = normalizeApiBaseUrl(apiBaseUrl); - const repo = normalizeRepoFullName(repoFullName); - if (typeof proposalId !== "string" || !proposalId.trim()) - throw new Error("invalid_proposal_id"); - if (!proposalStatusSet.has(status)) - throw new Error("invalid_proposal_status"); - setStatusStatement.run(status, new Date().toISOString(), forge, repo, proposalId.trim()); - }, - resolveEffectiveRules(repoFullName, options = {}) { - const proposals = store.listProposals(repoFullName, options.apiBaseUrl); - return resolveEffectiveDenyRules({ - includeDefaults: options.includeDefaults, - approvedProposals: proposals, - }); - }, - close() { - db.close(); - }, - }; - return store; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVueS1ob29rLXN5bnRoZXNpcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRlbnktaG9vay1zeW50aGVzaXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0hBQWdIO0FBQ2hILDRHQUE0RztBQUM1Ryw0R0FBNEc7QUFDNUcsa0dBQWtHO0FBQ2xHLCtGQUErRjtBQUMvRixPQUFPLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMvQyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQzFDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUNMLHVCQUF1QixFQUN2Qix1QkFBdUIsRUFDdkIscUJBQXFCLEVBQ3JCLHdCQUF3QixFQUN4QiwyQkFBMkIsRUFDM0IsdUJBQXVCLEVBQ3ZCLDZCQUE2QixFQUM3QixxQkFBcUIsRUFDckIsaUJBQWlCLEVBQ2pCLHlCQUF5QixFQUN6QixtQkFBbUIsRUFDbkIsMkJBQTJCLElBQUksaUNBQWlDLEdBQ2pFLE1BQU0sa0JBQWtCLENBQUM7QUFFMUIsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFHekQsNEdBQTRHO0FBQzVHLCtHQUErRztBQUMvRyxrR0FBa0c7QUFDbEcsT0FBTyxFQUNMLHVCQUF1QixFQUN2Qix1QkFBdUIsRUFDdkIscUJBQXFCLEVBQ3JCLHdCQUF3QixFQUN4QiwyQkFBMkIsRUFDM0IsdUJBQXVCLEVBQ3ZCLDZCQUE2QixFQUM3Qix5QkFBeUIsRUFDekIsbUJBQW1CLEdBQ3BCLENBQUM7QUF3QkYsTUFBTSxpQkFBaUIsR0FBRyw2QkFBNkIsQ0FBQztBQUV4RDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSwyQkFBMkIsQ0FBQyxPQUFnQixFQUFFLFNBQTBCLEVBQUU7SUFDeEYsT0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/deny-hooks.d.ts b/packages/loopover-miner/lib/deny-hooks.d.ts deleted file mode 100644 index 1b940a827f..0000000000 --- a/packages/loopover-miner/lib/deny-hooks.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { DEFAULT_DENY_RULES, evaluateDenyHooks, type DenyRule, type DenyVerdict, type ProposedToolCall, } from "@loopover/engine"; diff --git a/packages/loopover-miner/lib/deny-hooks.js b/packages/loopover-miner/lib/deny-hooks.js deleted file mode 100644 index 9612109588..0000000000 --- a/packages/loopover-miner/lib/deny-hooks.js +++ /dev/null @@ -1,7 +0,0 @@ -// PreToolUse-style deny-hook primitives (#2295). Now a thin re-export of the engine's pure, deterministic deny -// evaluator: the whole implementation moved into `@loopover/engine` (packages/loopover-engine/src/miner/ -// deny-hooks.ts) by #5667 so the review stack and the miner share one copy. No behavior change — the evaluator is -// pure (no IO, no globals, no Date/random). Types (DenyRule/DenyVerdict/ProposedToolCall) come from the same -// engine module so the miner package's public contract stays identical after the TypeScript migration. -export { DEFAULT_DENY_RULES, evaluateDenyHooks, } from "@loopover/engine"; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVueS1ob29rcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRlbnktaG9va3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0dBQStHO0FBQy9HLHlHQUF5RztBQUN6RyxrSEFBa0g7QUFDbEgsNkdBQTZHO0FBQzdHLHVHQUF1RztBQUV2RyxPQUFPLEVBQ0wsa0JBQWtCLEVBQ2xCLGlCQUFpQixHQUlsQixNQUFNLGtCQUFrQixDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/deployment-docs-audit.d.ts b/packages/loopover-miner/lib/deployment-docs-audit.d.ts deleted file mode 100644 index 13ff92302c..0000000000 --- a/packages/loopover-miner/lib/deployment-docs-audit.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** Parsed claims a DEPLOYMENT.md makes about the miner's runtime surface. */ -export type DeploymentDocsClaims = { - envVars: string[]; - filePaths: string[]; - subcommands: string[]; -}; -/** Filesystem-independent view of the live source tree the parsed claims are checked against. */ -export type DeploymentDocsReality = { - hasEnvRead: (name: string) => boolean; - /** The enumerable set of every real env-var read, so the reverse audit direction (#6601) can diff a real - * `LOOPOVER_MINER_*` read that DEPLOYMENT.md never documents, not just probe one documented name at a time. */ - envReads: Iterable; - pathExists: (relativePath: string) => boolean; - isRegisteredCommand: (name: string) => boolean; -}; -/** Result of cross-checking claims against reality: `ok` plus a message per stale claim. */ -export type DeploymentDocsAuditResult = { - ok: boolean; - failures: string[]; -}; -/** Collect every LOOPOVER_MINER_* / MINER_* token that appears in `text` (doc prose/code or source). */ -export declare function scanEnvVarTokens(text: string): Set; -/** Sorted, de-duplicated env-var names DEPLOYMENT.md claims the miner honors. */ -export declare function extractEnvVarClaims(markdown: string): string[]; -/** Sorted, de-duplicated `loopover-miner ` subcommands DEPLOYMENT.md documents. */ -export declare function extractSubcommandClaims(markdown: string): string[]; -/** True when a markdown link target is an on-disk repo path (not a URL, anchor, or runtime path). */ -export declare function isRepoRelativePath(target: string): boolean; -/** Sorted, de-duplicated repo-relative file paths DEPLOYMENT.md links to (external issue links excluded). - * An in-file anchor fragment (`file.md#heading`) is stripped before the path is recorded -- the fragment - * names a heading inside the target file, not a filesystem entry, so checking it against `pathExists` - * verbatim would always fail even when the linked file (and heading) both genuinely exist. */ -export declare function extractFilePathClaims(markdown: string): string[]; -/** The set of top-level subcommands the miner CLI dispatches, parsed from its bin entry source. */ -export declare function scanRegisteredCommands(binSource: string): Set; -/** - * Cross-check parsed DEPLOYMENT.md claims against reality. `reality` supplies three predicates so this - * comparison stays pure and filesystem-independent: `hasEnvRead(name)` (a read of that env var exists - * under packages/loopover-miner/**), `pathExists(relativePath)` (the doc-relative path is on disk), - * and `isRegisteredCommand(name)` (the subcommand is dispatched by the CLI). Returns the drift findings, - * each failure naming the specific stale claim rather than a generic mismatch. - */ -export declare function auditDeploymentDocs(claims: DeploymentDocsClaims, reality: DeploymentDocsReality): DeploymentDocsAuditResult; -/** Run the audit and throw a build-failing error naming every stale claim; returns the result when in sync. */ -export declare function assertDeploymentDocsInSync(claims: DeploymentDocsClaims, reality: DeploymentDocsReality): DeploymentDocsAuditResult; diff --git a/packages/loopover-miner/lib/deployment-docs-audit.js b/packages/loopover-miner/lib/deployment-docs-audit.js deleted file mode 100644 index 42a4f30c66..0000000000 --- a/packages/loopover-miner/lib/deployment-docs-audit.js +++ /dev/null @@ -1,109 +0,0 @@ -// Docs-accuracy audit for the miner's DEPLOYMENT.md (#5180). Mirrors the self-host docs audit -// (apps/loopover-ui/src/lib/selfhost-docs-audit.ts): parse the deployment doc, then assert every -// LOOPOVER_MINER_* / MINER_* env var, repo-relative file path, and `loopover-miner ` -// it documents still exists under packages/loopover-miner/**. A rename or move that leaves the doc -// stale then fails CI with a message naming the exact stale claim, instead of misleading operators. -// Wired into CI via `npm run test:miner-deployment-docs-audit` (scripts/check-miner-deployment-docs.mjs) -// and the live unit suite in test/unit/miner-deployment-docs-audit.test.ts (#6158). -/** The miner's own env-var namespace: LOOPOVER_MINER_* and the shorter MINER_* aliases it reads. */ -const ENV_VAR_PATTERN = /\b(?:LOOPOVER_MINER|MINER)_[A-Z0-9_]+\b/g; -/** `loopover-miner ` CLI invocations, excluding the `@loopover/miner` package spelling. */ -const SUBCOMMAND_PATTERN = /(?"` guards in the miner bin — the CLI's registered top-level command table. */ -const CLI_DISPATCH_PATTERN = /cliArgs\[0\]\s*===\s*"([a-z][a-z0-9-]*)"/g; -/** Collect every LOOPOVER_MINER_* / MINER_* token that appears in `text` (doc prose/code or source). */ -export function scanEnvVarTokens(text) { - const tokens = new Set(); - for (const match of text.matchAll(ENV_VAR_PATTERN)) { - tokens.add(match[0]); - } - return tokens; -} -/** Sorted, de-duplicated env-var names DEPLOYMENT.md claims the miner honors. */ -export function extractEnvVarClaims(markdown) { - return [...scanEnvVarTokens(markdown)].sort(); -} -/** Sorted, de-duplicated `loopover-miner ` subcommands DEPLOYMENT.md documents. */ -export function extractSubcommandClaims(markdown) { - const commands = new Set(); - for (const match of markdown.matchAll(SUBCOMMAND_PATTERN)) { - commands.add(match[1]); - } - return [...commands].sort(); -} -/** True when a markdown link target is an on-disk repo path (not a URL, anchor, or runtime path). */ -export function isRepoRelativePath(target) { - return !NON_REPO_LINK_PATTERN.test(target); -} -/** Sorted, de-duplicated repo-relative file paths DEPLOYMENT.md links to (external issue links excluded). - * An in-file anchor fragment (`file.md#heading`) is stripped before the path is recorded -- the fragment - * names a heading inside the target file, not a filesystem entry, so checking it against `pathExists` - * verbatim would always fail even when the linked file (and heading) both genuinely exist. */ -export function extractFilePathClaims(markdown) { - const paths = new Set(); - for (const match of markdown.matchAll(MARKDOWN_LINK_PATTERN)) { - const target = match[1].trim(); - if (isRepoRelativePath(target)) { - const [pathOnly] = target.split("#"); - paths.add(pathOnly); - } - } - return [...paths].sort(); -} -/** The set of top-level subcommands the miner CLI dispatches, parsed from its bin entry source. */ -export function scanRegisteredCommands(binSource) { - const commands = new Set(); - for (const match of binSource.matchAll(CLI_DISPATCH_PATTERN)) { - commands.add(match[1]); - } - return commands; -} -/** - * Cross-check parsed DEPLOYMENT.md claims against reality. `reality` supplies three predicates so this - * comparison stays pure and filesystem-independent: `hasEnvRead(name)` (a read of that env var exists - * under packages/loopover-miner/**), `pathExists(relativePath)` (the doc-relative path is on disk), - * and `isRegisteredCommand(name)` (the subcommand is dispatched by the CLI). Returns the drift findings, - * each failure naming the specific stale claim rather than a generic mismatch. - */ -export function auditDeploymentDocs(claims, reality) { - const failures = []; - for (const name of claims.envVars) { - if (!reality.hasEnvRead(name)) { - failures.push(`env var "${name}" is documented in DEPLOYMENT.md but no read of it exists under packages/loopover-miner/**`); - } - } - for (const path of claims.filePaths) { - if (!reality.pathExists(path)) { - failures.push(`file path "${path}" is linked from DEPLOYMENT.md but no longer exists on disk`); - } - } - for (const command of claims.subcommands) { - if (!reality.isRegisteredCommand(command)) { - failures.push(`CLI subcommand "loopover-miner ${command}" is documented in DEPLOYMENT.md but is not registered in the CLI command table`); - } - } - // Reverse direction (#6601): a real `LOOPOVER_MINER_*` env-var read that DEPLOYMENT.md never documents. Scoped - // to the `LOOPOVER_MINER_` prefix and excluding the `*_DB` family (documented generically via one pattern - // sentence, not enumerated) and the bare `MINER_*` alias namespace (which also matches non-env event/metric/ - // filename constants). `reality.envReads` is the enumerable set of real reads the forward `hasEnvRead` probes. - const documented = new Set(claims.envVars); - for (const name of reality.envReads) { - if (name.startsWith("LOOPOVER_MINER_") && !name.endsWith("_DB") && !documented.has(name)) { - failures.push(`env var "${name}" is read under packages/loopover-miner/** but is not documented in DEPLOYMENT.md`); - } - } - return { ok: failures.length === 0, failures }; -} -/** Run the audit and throw a build-failing error naming every stale claim; returns the result when in sync. */ -export function assertDeploymentDocsInSync(claims, reality) { - const result = auditDeploymentDocs(claims, reality); - if (!result.ok) { - throw new Error(`DEPLOYMENT.md is out of sync with packages/loopover-miner/**:\n- ${result.failures.join("\n- ")}`); - } - return result; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95bWVudC1kb2NzLWF1ZGl0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVwbG95bWVudC1kb2NzLWF1ZGl0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDhGQUE4RjtBQUM5RixpR0FBaUc7QUFDakcsaUdBQWlHO0FBQ2pHLG1HQUFtRztBQUNuRyxvR0FBb0c7QUFDcEcseUdBQXlHO0FBQ3pHLG9GQUFvRjtBQXlCcEYsb0dBQW9HO0FBQ3BHLE1BQU0sZUFBZSxHQUFHLDBDQUEwQyxDQUFDO0FBRW5FLHVHQUF1RztBQUN2RyxNQUFNLGtCQUFrQixHQUFHLGtEQUFrRCxDQUFDO0FBRTlFLGlFQUFpRTtBQUNqRSxNQUFNLHFCQUFxQixHQUFHLGdCQUFnQixDQUFDO0FBRS9DLDBHQUEwRztBQUMxRyxNQUFNLHFCQUFxQixHQUFHLGlDQUFpQyxDQUFDO0FBRWhFLHdHQUF3RztBQUN4RyxNQUFNLG9CQUFvQixHQUFHLDJDQUEyQyxDQUFDO0FBRXpFLHdHQUF3RztBQUN4RyxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsSUFBWTtJQUMzQyxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsRUFBVSxDQUFDO0lBQ2pDLEtBQUssTUFBTSxLQUFLLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxlQUFlLENBQUMsRUFBRSxDQUFDO1FBQ25ELE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdkIsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUM7QUFFRCxpRkFBaUY7QUFDakYsTUFBTSxVQUFVLG1CQUFtQixDQUFDLFFBQWdCO0lBQ2xELE9BQU8sQ0FBQyxHQUFHLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDaEQsQ0FBQztBQUVELCtGQUErRjtBQUMvRixNQUFNLFVBQVUsdUJBQXVCLENBQUMsUUFBZ0I7SUFDdEQsTUFBTSxRQUFRLEdBQUcsSUFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/deployment-docs-audit.ts b/packages/loopover-miner/lib/deployment-docs-audit.ts index ada95287ae..54a9830f31 100644 --- a/packages/loopover-miner/lib/deployment-docs-audit.ts +++ b/packages/loopover-miner/lib/deployment-docs-audit.ts @@ -3,7 +3,7 @@ // LOOPOVER_MINER_* / MINER_* env var, repo-relative file path, and `loopover-miner ` // it documents still exists under packages/loopover-miner/**. A rename or move that leaves the doc // stale then fails CI with a message naming the exact stale claim, instead of misleading operators. -// Wired into CI via `npm run test:miner-deployment-docs-audit` (scripts/check-miner-deployment-docs.mjs) +// Wired into CI via `npm run test:miner-deployment-docs-audit` (scripts/check-miner-deployment-docs.ts) // and the live unit suite in test/unit/miner-deployment-docs-audit.test.ts (#6158). /** Parsed claims a DEPLOYMENT.md makes about the miner's runtime surface. */ diff --git a/packages/loopover-miner/lib/discover-cli.d.ts b/packages/loopover-miner/lib/discover-cli.d.ts deleted file mode 100644 index e72237e64b..0000000000 --- a/packages/loopover-miner/lib/discover-cli.d.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { ForgeConfig } from "./forge-config.js"; -import type { CandidateIssueWarning, FanoutOptions, FanoutTarget, RawCandidateIssue } from "./opportunity-fanout.js"; -import type { RankCandidateIssuesOptions, RankedCandidateIssue, RankedCandidateSummary } from "./opportunity-ranker.js"; -import type { PolicyDocCacheStore } from "./policy-doc-cache.js"; -import type { PolicyVerdictCacheStore } from "./policy-verdict-cache.js"; -import type { EnqueueRankedDiscoverySummary } from "./portfolio-discovery.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -import type { RankedCandidatesStore } from "./ranked-candidates.js"; -import type { queryDiscoveryIndex as QueryDiscoveryIndexFn } from "./discovery-index-client.js"; -export type ParsedDiscoverArgs = { - targets: FanoutTarget[]; - search: string | null; - dryRun: boolean; - json: boolean; - /** Present only when `--api-base-url` is supplied (#4784); threads the tenant's forge host to the fan-out. */ - apiBaseUrl?: string; - /** Present only when `--token-env` is supplied (#4784); names the credential env var to read. */ - tokenEnv?: string; -} | { - error: string; -}; -/** The subset of `CandidateIssueSummary` runDiscover actually reads. It surfaces the rate-limit telemetry (#4837), - * so a fake must supply it. A real `fetchCandidateIssuesWithSummary` result satisfies this, since it is a superset. */ -export type DiscoverFanOutSummary = { - issues: RawCandidateIssue[]; - warnings: CandidateIssueWarning[]; - rateLimitRemaining: number | null; - rateLimitResetAt: string | null; -}; -/** The subset of a ranked entry that `renderDiscoverSummary` reads for its top-candidates listing. */ -export type DiscoverRankedEntry = Pick; -export type DiscoverResult = { - fanOutCount: number; - warnings: CandidateIssueWarning[]; - rateLimitRemaining: number | null; - rateLimitResetAt: string | null; - ranked: DiscoverRankedEntry[]; - /** Candidates the eligibility filter dropped, each with the repo/issue and the reason (#6798). */ - excluded?: Array<{ - repoFullName: string; - issueNumber: number; - reason: string; - }>; - /** True when ranking fell back to the built-in default goal spec because no per-tenant spec was supplied (#4784). */ - usedDefaultGoalSpec?: boolean; - enqueueSummary: EnqueueRankedDiscoverySummary; -}; -export type RunDiscoverOptions = { - /** Read for the discovery-index opt-in gate (#7168) -- defaults to `process.env`. */ - env?: Record; - githubToken?: string; - apiBaseUrl?: string; - /** Per-tenant credential env var name (#4784); defaults to GITHUB_TOKEN. Overridden by a `--token-env` flag. */ - tokenEnv?: string; - /** Per-tenant forge knobs beyond the host (#4784), forwarded to the fan-out. */ - forge?: Partial; - nowMs?: number; - /** Per-tenant goal specs threaded to the ranker so lane fit uses the tenant's conventions, not the defaults (#4784). */ - goalSpecsByRepo?: RankCandidateIssuesOptions["goalSpecsByRepo"]; - goalSpecContentByRepo?: RankCandidateIssuesOptions["goalSpecContentByRepo"]; - initPortfolioQueue?: () => PortfolioQueueStore; - initPolicyDocCache?: () => PolicyDocCacheStore; - initPolicyVerdictCache?: () => PolicyVerdictCacheStore; - initRankedCandidatesStore?: () => RankedCandidatesStore; - fetchCandidateIssuesWithSummary?: (targets: FanoutTarget[], githubToken: string, options?: FanoutOptions) => Promise; - searchCandidateIssuesWithSummary?: (searchQuery: string, githubToken: string, options?: FanoutOptions) => Promise; - rankCandidateIssuesWithSummary?: (candidates: RawCandidateIssue[], options?: RankCandidateIssuesOptions) => RankedCandidateSummary; - enqueueRankedDiscovery?: (rankedIssues: RankedCandidateIssue[], options: { - queueStore: PortfolioQueueStore; - }) => EnqueueRankedDiscoverySummary; - /** Supplements the local fan-out with hosted discovery-index results for the same scope, when the plane is - * enabled (#7168). Defaults to discovery-index-client.js's own queryDiscoveryIndex. */ - queryDiscoveryIndex?: typeof QueryDiscoveryIndexFn; - /** Invoked with the real structured result at each success return point (dry-run and full-run), in addition - * to (never instead of) the plain exit-code return -- mirrors `RunAttemptOptions.onResult`. Never fires on a - * parse-error/unexpected-error `reportCliFailure` branch, matching runAttempt's own asymmetry (#6522). */ - onResult?: (result: DiscoverResult) => void; - /** Resolve each candidate repo's ContributionProfile for eligibility filtering (#6798). Defaults to - * resolveContributionProfilesForDiscover; injectable so tests avoid the network. */ - resolveContributionProfiles?: (repoFullNames: string[], ctx: { - githubToken?: string; - apiBaseUrl?: string; - nowMs?: number; - }) => Promise>; -}; -export declare function sanitizeDiscoverDisplayText(value: unknown): string; -export declare function parseDiscoverArgs(args: string[]): ParsedDiscoverArgs; -export declare function renderDiscoverSummary(result: DiscoverResult): string; -/** - * Default per-repo ContributionProfile resolver (#6798): reads the local cache and, on a miss/stale entry, - * extracts a fresh profile and caches it. Returns a Map keyed by repoFullName. - * - * WITHOUT a github token this returns an empty map and does no network work at all — AMS can't reliably read a - * repo's label taxonomy/docs unauthenticated (rate limits), so it safe-defaults to no eligibility filtering. - * That also keeps callers that don't supply a token (the common CLI path, and every test) hermetic. - * - * @param {string[]} repoFullNames unique repos among the fanned-out candidates - * @param {{ githubToken?: string, apiBaseUrl?: string, nowMs?: number, initCache?: typeof initContributionProfileCache, extract?: typeof extractContributionProfile }} ctx - * @returns {Promise>} - */ -export declare function resolveContributionProfilesForDiscover(repoFullNames: string[], ctx?: { - githubToken?: string; - apiBaseUrl?: string; - nowMs?: number; - initCache?: unknown; - extract?: unknown; -}): Promise>; -export declare function runDiscover(args: string[], options?: RunDiscoverOptions): Promise; diff --git a/packages/loopover-miner/lib/discover-cli.js b/packages/loopover-miner/lib/discover-cli.js deleted file mode 100644 index fce13f3045..0000000000 --- a/packages/loopover-miner/lib/discover-cli.js +++ /dev/null @@ -1,458 +0,0 @@ -/** `discover` CLI command (#4247): wires the existing fanout -> rank -> enqueue pipeline together so a miner - * can actually run it. Every piece already exists and is independently tested; this module only composes them. */ -import { resolveForgeConfig } from "./forge-config.js"; -import { fetchCandidateIssuesWithSummary, searchCandidateIssuesWithSummary, } from "./opportunity-fanout.js"; -import { rankCandidateIssuesWithSummary } from "./opportunity-ranker.js"; -import { initPolicyDocCacheStore } from "./policy-doc-cache.js"; -import { initPolicyVerdictCacheStore } from "./policy-verdict-cache.js"; -import { enqueueRankedDiscovery } from "./portfolio-discovery.js"; -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { initRankedCandidatesStore } from "./ranked-candidates.js"; -import { extractContributionProfile } from "./contribution-profile-extract.js"; -import { initContributionProfileCache } from "./contribution-profile-cache.js"; -import { filterCandidatesByProfiles } from "./contribution-profile-filter.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { isDiscoveryPlaneEnabled, queryDiscoveryIndex, recordDiscoveryTelemetry } from "./discovery-index-client.js"; -const DISCOVER_USAGE = "Usage: loopover-miner discover [...] | --search [--dry-run] [--json] [--api-base-url ] [--token-env ]"; -const MAX_DISCOVER_TITLE_DISPLAY_LENGTH = 240; -const OSC_SEQUENCE_PATTERN = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g; -const ANSI_ESCAPE_PATTERN = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g; -const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/g; -const BIDI_CONTROL_PATTERN = /[\u200e\u200f\u202a-\u202e\u2066-\u2069]/g; -export function sanitizeDiscoverDisplayText(value) { - return String(value ?? "") - .replace(OSC_SEQUENCE_PATTERN, "") - .replace(ANSI_ESCAPE_PATTERN, "") - .replace(CONTROL_CHARACTER_PATTERN, " ") - .replace(BIDI_CONTROL_PATTERN, "") - .replace(/\s+/g, " ") - .trim() - .slice(0, MAX_DISCOVER_TITLE_DISPLAY_LENGTH); -} -function dedupeKey(repoFullName, issueNumber) { - return `${repoFullName.toLowerCase()}#${issueNumber}`; -} -/** - * Supplements `fanOut.issues` with hosted discovery-index results for the same scope (#7168) -- a complete - * no-op (returns `fanOut` unchanged) unless the plane is enabled, so a run with the flag unset behaves exactly - * as before this feature existed. Local results always win on a duplicate issue (the discovery-index candidate - * is dropped, not merged over it) -- this instance's own live fan-out is more current than a cached shared - * index entry. Discovery-index candidates now carry their real `assignees` when the hosted contract supplies them - * (#7442): the value flows through so contribution-profile-filter.js's repo-owner exclusion (#7040) engages for - * index-sourced candidates exactly as it does for direct fan-out ones. A candidate whose response omitted the - * field (older discovery-index build) falls back to `[]` -- fail-safe: the filter still runs, it just can't detect - * an owner-assignment it was never told about, rather than the check being silently skipped. - */ -async function supplementWithDiscoveryIndex(fanOut, queryScope, options) { - const env = options.env ?? process.env; - if (!isDiscoveryPlaneEnabled(env)) - return fanOut; - const queryIndex = options.queryDiscoveryIndex ?? queryDiscoveryIndex; - const response = await queryIndex(queryScope, { env }); - recordDiscoveryTelemetry("discover_query", response.candidates.length > 0 ? "supplemented" : "empty", { env }); - if (response.candidates.length === 0) - return fanOut; - const seen = new Set(fanOut.issues.map((issue) => dedupeKey(issue.repoFullName, issue.issueNumber))); - const supplemented = response.candidates - .filter((candidate) => !seen.has(dedupeKey(candidate.repoFullName, candidate.issueNumber))) - // DiscoveryIndexCandidate is a near-superset of RawCandidateIssue; copy the real assignees through when the - // hosted contract carried them (#7442), falling back to [] only when the served response genuinely omitted the - // field — cast preserves pre-existing runtime shape rather than re-mapping. - .map((candidate) => ({ ...candidate, assignees: [...(candidate.assignees ?? [])], labels: [...candidate.labels] })); - if (supplemented.length === 0) - return fanOut; - return { ...fanOut, issues: [...fanOut.issues, ...supplemented] }; -} -function parseRepoTarget(value) { - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - return { owner, repo }; -} -export function parseDiscoverArgs(args) { - // `--api-base-url` and `--token-env` (#4784) thread the tenant's forge host and credential env var into the - // fan-out; they are kept off the parsed result unless supplied, so callers that pass neither see the exact - // pre-#4784 `{ targets, search, json }` shape. - const options = { json: false, dryRun: false, search: null, apiBaseUrl: null, tokenEnv: null }; - const targets = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: fetches + ranks exactly as a real run, but skips opening any local store and makes zero writes. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--search") { - const query = args[index + 1]; - if (!query || query.startsWith("-")) - return { error: DISCOVER_USAGE }; - options.search = query; - index += 1; - continue; - } - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: DISCOVER_USAGE }; - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token === "--token-env") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: DISCOVER_USAGE }; - options.tokenEnv = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - const target = parseRepoTarget(token); - if (!target) - return { error: `Repository must be in owner/repo form: ${token}` }; - targets.push(target); - } - if (options.search === null && targets.length === 0) { - return { error: DISCOVER_USAGE }; - } - if (options.search !== null && targets.length > 0) { - return { error: "Pass either repository targets or --search, not both." }; - } - return { - targets, - search: options.search, - dryRun: options.dryRun, - json: options.json, - ...(options.apiBaseUrl !== null ? { apiBaseUrl: options.apiBaseUrl } : {}), - ...(options.tokenEnv !== null ? { tokenEnv: options.tokenEnv } : {}), - }; -} -// The rate-limit line surfaces the telemetry the fanout already records (#4837) so an operator sees how close a -// `discover` run is to being throttled without running a separate command. `unknown` covers the no-fetch/no-header -// case where the fanout captured no remaining count. -function renderRateLimitLine(result) { - const remaining = result.rateLimitRemaining === null ? "unknown" : String(result.rateLimitRemaining); - const resetSuffix = result.rateLimitResetAt === null ? "" : ` (resets ${result.rateLimitResetAt})`; - return `rate-limit remaining: ${remaining}${resetSuffix}`; -} -export function renderDiscoverSummary(result) { - const lines = [ - `fanned out: ${result.fanOutCount} candidate issue(s)`, - `ai-policy warnings: ${result.warnings.length}`, - `ranked: ${result.ranked.length}`, - `enqueued: ${result.enqueueSummary.enqueued}`, - renderRateLimitLine(result), - ]; - if (result.enqueueSummary.skippedBelowMinRank > 0) { - lines.push(`skipped (below min rank): ${result.enqueueSummary.skippedBelowMinRank}`); - } - // #6798: surface what the eligibility filter dropped and why, so a human sees AMS's inference. - const excluded = result.excluded ?? []; - if (excluded.length > 0) { - lines.push(`excluded (eligibility): ${excluded.length}`); - for (const entry of excluded.slice(0, 10)) { - lines.push(` ${entry.repoFullName}#${entry.issueNumber} ${entry.reason}`); - } - } - // Make the fall-back to loopover's built-in rubric explicit instead of silent (#4784): when no per-tenant goal - // spec is supplied, lane fit reflects loopover's defaults, not the target repo's own conventions. - if (result.usedDefaultGoalSpec) { - lines.push("note: ranked with the built-in default goal spec (no per-tenant .loopover-miner.yml supplied)"); - } - if (result.ranked.length === 0) { - lines.push("", "no candidates found."); - return lines.join("\n"); - } - lines.push("", "top candidates:"); - for (const entry of result.ranked.slice(0, 10)) { - const title = sanitizeDiscoverDisplayText(entry.title); - lines.push(` ${entry.repoFullName}#${entry.issueNumber} score=${entry.rankScore.toFixed(4)} ${title}`); - } - return lines.join("\n"); -} -/** - * Default per-repo ContributionProfile resolver (#6798): reads the local cache and, on a miss/stale entry, - * extracts a fresh profile and caches it. Returns a Map keyed by repoFullName. - * - * WITHOUT a github token this returns an empty map and does no network work at all — AMS can't reliably read a - * repo's label taxonomy/docs unauthenticated (rate limits), so it safe-defaults to no eligibility filtering. - * That also keeps callers that don't supply a token (the common CLI path, and every test) hermetic. - * - * @param {string[]} repoFullNames unique repos among the fanned-out candidates - * @param {{ githubToken?: string, apiBaseUrl?: string, nowMs?: number, initCache?: typeof initContributionProfileCache, extract?: typeof extractContributionProfile }} ctx - * @returns {Promise>} - */ -export async function resolveContributionProfilesForDiscover(repoFullNames, ctx = {}) { - const profiles = new Map(); - if (!ctx.githubToken) - return profiles; - const initCache = ctx.initCache ?? initContributionProfileCache; - const extract = ctx.extract ?? extractContributionProfile; - const cache = initCache(); - try { - for (const repoFullName of repoFullNames) { - const cached = cache.get(repoFullName, ctx.nowMs); - if (cached && !cached.stale) { - profiles.set(repoFullName, cached.profile); - continue; - } - const profile = await extract(repoFullName, { - githubToken: ctx.githubToken, - // exactOptionalPropertyTypes: omit apiBaseUrl when unset (pre-existing optional-prop shape). - ...(ctx.apiBaseUrl !== undefined ? { apiBaseUrl: ctx.apiBaseUrl } : {}), - }); - cache.put(profile, ctx.nowMs); - profiles.set(repoFullName, profile); - } - } - finally { - cache.close(); - } - return profiles; -} -export async function runDiscover(args, options = {}) { - const parsed = parseDiscoverArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - // Credential env var is per-tenant (#4784): a `--token-env FORGE_PAT` flag (or `options.tokenEnv`) reads a - // non-`GITHUB_TOKEN` variable so a non-github.com forge's token is reachable. The default falls through to the - // forge adapter's own `tokenEnvVar` (github.com's `GITHUB_TOKEN`), so there's a single source of truth for the - // default credential env instead of a second hardcoded literal that could drift from `DEFAULT_FORGE_CONFIG`. - const tokenEnv = parsed.tokenEnv ?? options.tokenEnv ?? resolveForgeConfig(options.forge).tokenEnvVar; - const githubToken = options.githubToken ?? process.env[tokenEnv] ?? ""; - // A `--api-base-url` flag (or `options.apiBaseUrl`) surfaces the fan-out's existing forge-host override at the CLI - // (#4784); `options.forge` carries any remaining per-tenant forge knobs for a programmatic caller. - const apiBaseUrl = parsed.apiBaseUrl ?? options.apiBaseUrl; - const fetchTargets = options.fetchCandidateIssuesWithSummary ?? fetchCandidateIssuesWithSummary; - const searchTargets = options.searchCandidateIssuesWithSummary ?? searchCandidateIssuesWithSummary; - const rankIssues = options.rankCandidateIssuesWithSummary ?? rankCandidateIssuesWithSummary; - const enqueue = options.enqueueRankedDiscovery ?? enqueueRankedDiscovery; - // Eligibility filtering (#6798): resolve each candidate repo's ContributionProfile and drop candidates the - // repo's own conventions would reject, BEFORE ranking. Safe by default -- see resolveContributionProfilesForDiscover. - const resolveProfiles = options.resolveContributionProfiles ?? resolveContributionProfilesForDiscover; - // Same scope this run already asks GitHub about (#7168) -- the discovery-index supplement, when enabled, - // asks the shared hosted index about the identical targets/search rather than a different query entirely. - const discoveryQueryScope = parsed.search !== null - ? { repos: [], orgs: [], searchTerms: [parsed.search] } - : { repos: parsed.targets.map((target) => `${target.owner}/${target.repo}`), orgs: [], searchTerms: [] }; - // #4847: fetch + rank are read-only GitHub GETs and pure local computation, so a dry run still does them for - // real (that's the useful "what would this discover?" output) -- but it never opens any local store (portfolio - // queue, policy-doc cache, policy-verdict cache), since opening a not-yet-existing SQLite store file is itself - // a write. The ranked issues are fed through a no-op queue stub so enqueueRankedDiscovery's own classification - // logic (valid/invalid, below-min-rank) still runs for real, just without ever touching the real queue. - if (parsed.dryRun) { - // exactOptionalPropertyTypes: cast through FanoutOptions — apiBaseUrl/forge may be unset at runtime. - const fanOutOptions = { - apiBaseUrl, - forge: options.forge, - policyDocCache: null, - policyVerdictCache: null, - }; - try { - let fanOut = parsed.search !== null - ? await searchTargets(parsed.search, githubToken, fanOutOptions) - : await fetchTargets(parsed.targets, githubToken, fanOutOptions); - fanOut = await supplementWithDiscoveryIndex(fanOut, discoveryQueryScope, options); - // #6798: same eligibility filter as the real path, so a dry run shows the exact candidate set a real run - // would enqueue (and the same excluded set), rather than an unfiltered preview. - const repoFullNames = [...new Set(fanOut.issues.map((issue) => issue.repoFullName))]; - const profilesByRepo = await resolveProfiles(repoFullNames, { - githubToken, - ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}), - ...(options.nowMs !== undefined ? { nowMs: options.nowMs } : {}), - }); - // RunDiscoverOptions.resolveContributionProfiles is typed as Map (pre-existing .d.ts); - // the filter expects ContributionProfile values — same runtime objects. - const { kept, excluded } = filterCandidatesByProfiles(fanOut.issues, profilesByRepo); - const rankedSummary = rankIssues(kept, { - ...(options.nowMs !== undefined ? { nowMs: options.nowMs } : {}), - ...(options.goalSpecsByRepo !== undefined ? { goalSpecsByRepo: options.goalSpecsByRepo } : {}), - ...(options.goalSpecContentByRepo !== undefined - ? { goalSpecContentByRepo: options.goalSpecContentByRepo } - : {}), - }); - const noopQueueStore = { enqueue: () => { } }; - const enqueueSummary = enqueue(rankedSummary.issues, { queueStore: noopQueueStore }); - const result = { - outcome: "dry_run", - fanOutCount: fanOut.issues.length, - warnings: fanOut.warnings, - rateLimitRemaining: fanOut.rateLimitRemaining, - rateLimitResetAt: fanOut.rateLimitResetAt, - ranked: rankedSummary.issues, - excluded: excluded.map((entry) => ({ - repoFullName: entry.candidate.repoFullName, - issueNumber: entry.candidate.issueNumber, - reason: entry.reason, - })), - usedDefaultGoalSpec: rankedSummary.usedDefaultGoalSpec, - enqueueSummary, - }; - // Structured-outcome hook (#6522), mirroring runAttempt's onResult convention: fires only at a real - // structured success point (never the reportCliFailure branches), in addition to -- never instead of -- - // the plain exit-code return, so a non-CLI caller (the /api/discover route) can read the result. - // Dry-run result adds `outcome: "dry_run"` at runtime; DiscoverResult/.d.ts omits it — pre-existing drift. - options.onResult?.(result); - if (parsed.json) { - console.log(JSON.stringify(result, null, 2)); - } - else { - console.log(renderDiscoverSummary(result)); - console.log("\nDRY RUN: no portfolio-queue write was made."); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - } - const ownsPortfolioQueue = options.initPortfolioQueue === undefined; - let portfolioQueue; - try { - portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - // Local ETag cache so a repeated discover revalidates each repo's policy docs with a conditional GET instead of - // re-downloading them (#4842). Opened inside its OWN try/catch, separate from the portfolio queue above: the - // queue is required infrastructure (discovery genuinely cannot enqueue anything without it, so a real open - // failure should abort the run), but the policy-doc cache is a pure performance optimization -- a corrupt or - // unwritable cache DB must degrade to "no cache" (every doc fetched in full, exactly as before #4842) rather - // than fail discovery outright. - let policyDocCache = null; - let ownsPolicyDocCache = false; - try { - ownsPolicyDocCache = options.initPolicyDocCache === undefined; - policyDocCache = (options.initPolicyDocCache ?? initPolicyDocCacheStore)(); - } - catch { - policyDocCache = null; - ownsPolicyDocCache = false; - } - // Persisted cache of resolved policy verdicts (#4843), same "own try/catch, degrade to null" discipline as the - // doc cache above and for the same reason: purely a performance optimization the feature is inert without, so a - // corrupt/unwritable cache DB must never abort a run. - let policyVerdictCache = null; - let ownsPolicyVerdictCache = false; - try { - ownsPolicyVerdictCache = options.initPolicyVerdictCache === undefined; - policyVerdictCache = (options.initPolicyVerdictCache ?? initPolicyVerdictCacheStore)(); - } - catch { - policyVerdictCache = null; - ownsPolicyVerdictCache = false; - } - // Snapshot of this run's full ranked output (#4859 prerequisite), so a local HTTP endpoint (and eventually the - // miner-ui/browser-extension live-fetch it's meant for) can serve the same per-issue breakdown `--json` prints, - // without the operator re-running discover or hand-pasting its output. Same "own try/catch, degrade to null" - // discipline as the two caches above: a corrupt/unwritable snapshot store must never abort discovery's actual - // job (fan out, rank, enqueue). Unlike the caches, this store is a WRITE target, not a read optimization -- the - // save call itself gets its own try/catch below for the same reason. - let rankedCandidatesStore = null; - let ownsRankedCandidatesStore = false; - try { - ownsRankedCandidatesStore = options.initRankedCandidatesStore === undefined; - rankedCandidatesStore = (options.initRankedCandidatesStore ?? initRankedCandidatesStore)(); - } - catch { - rankedCandidatesStore = null; - ownsRankedCandidatesStore = false; - } - const fanOutOptions = { - apiBaseUrl, - forge: options.forge, - policyDocCache, - policyVerdictCache, - }; - try { - let fanOut = parsed.search !== null - ? await searchTargets(parsed.search, githubToken, fanOutOptions) - : await fetchTargets(parsed.targets, githubToken, fanOutOptions); - fanOut = await supplementWithDiscoveryIndex(fanOut, discoveryQueryScope, options); - // Eligibility filter (#6798): drop candidates a target repo's own conventions would reject, before ranking. - // A repo with no trustworthy eligibility profile keeps every candidate (filterCandidatesByProfiles' safe - // default), so this never silently skips real work on a repo whose conventions AMS couldn't read. - const repoFullNames = [...new Set(fanOut.issues.map((issue) => issue.repoFullName))]; - const profilesByRepo = await resolveProfiles(repoFullNames, { - githubToken, - ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}), - ...(options.nowMs !== undefined ? { nowMs: options.nowMs } : {}), - }); - // RunDiscoverOptions.resolveContributionProfiles is typed as Map (pre-existing .d.ts); - // the filter expects ContributionProfile values — same runtime objects. - const { kept, excluded } = filterCandidatesByProfiles(fanOut.issues, profilesByRepo); - // Pass any caller-supplied per-tenant goal specs through to the ranker so lane fit uses the tenant's - // conventions instead of silently falling back to loopover's defaults (#4784); the fallback is surfaced via - // `usedDefaultGoalSpec` below rather than hidden. - const rankedSummary = rankIssues(kept, { - ...(options.nowMs !== undefined ? { nowMs: options.nowMs } : {}), - ...(options.goalSpecsByRepo !== undefined ? { goalSpecsByRepo: options.goalSpecsByRepo } : {}), - ...(options.goalSpecContentByRepo !== undefined - ? { goalSpecContentByRepo: options.goalSpecContentByRepo } - : {}), - }); - const enqueueSummary = enqueue(rankedSummary.issues, { - queueStore: portfolioQueue, - ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}), - }); - try { - // Optional chaining rather than an `if (rankedCandidatesStore)` guard: a null store (open failed above) - // short-circuits to a no-op read, so the same try/catch below also covers the open-failed case without a - // second explicit branch. - rankedCandidatesStore?.saveRankedCandidates(rankedSummary.issues, options.nowMs); - } - catch { - // Non-fatal: the ranked-candidates snapshot is a nice-to-have for the local HTTP endpoint, not a - // requirement for discover's own job (fan out, rank, enqueue), which already succeeded above. - } - const result = { - fanOutCount: fanOut.issues.length, - warnings: fanOut.warnings, - rateLimitRemaining: fanOut.rateLimitRemaining, - rateLimitResetAt: fanOut.rateLimitResetAt, - ranked: rankedSummary.issues, - // #6798: candidates the eligibility filter dropped, each with the repo + issue + reason, so a human sees - // what AMS inferred and why a candidate was skipped. Empty when no profile was trustworthy enough to filter. - excluded: excluded.map((entry) => ({ - repoFullName: entry.candidate.repoFullName, - issueNumber: entry.candidate.issueNumber, - reason: entry.reason, - })), - usedDefaultGoalSpec: rankedSummary.usedDefaultGoalSpec, - enqueueSummary, - }; - // Structured-outcome hook (#6522) for the full-run success point -- same convention as the dry-run branch - // above and as runAttempt's onResult: real result only, additive to the unchanged exit-code return. - options.onResult?.(result); - if (parsed.json) { - console.log(JSON.stringify(result, null, 2)); - } - else { - console.log(renderDiscoverSummary(result)); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - if (ownsPortfolioQueue && portfolioQueue) - portfolioQueue.close(); - if (ownsPolicyDocCache && policyDocCache) - policyDocCache.close(); - if (ownsPolicyVerdictCache && policyVerdictCache) - policyVerdictCache.close(); - if (ownsRankedCandidatesStore && rankedCandidatesStore) - rankedCandidatesStore.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY292ZXItY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGlzY292ZXItY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO2tIQUNrSDtBQUNsSCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUV2RCxPQUFPLEVBQ0wsK0JBQStCLEVBQy9CLGdDQUFnQyxHQUNqQyxNQUFNLHlCQUF5QixDQUFDO0FBT2pDLE9BQU8sRUFBRSw4QkFBOEIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBTXpFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRWhFLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRXhFLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBRWxFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBRS9ELE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRW5FLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLG1DQUFtQyxDQUFDO0FBQy9FLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQy9FLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBRTlFLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRixPQUFPLEVBQUUsdUJBQXVCLEVBQUUsbUJBQW1CLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQW9HckgsTUFBTSxjQUFjLEdBQ2xCLGtKQUFrSixDQUFDO0FBRXJKLE1BQU0saUNBQWlDLEdBQUcsR0FBRyxDQUFDO0FBQzlDLE1BQU0sb0JBQW9CLEdBQUcsc0NBQXNDLENBQUM7QUFDcEUsTUFBTSxtQkFBbUIsR0FBRyxzQ0FBc0MsQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/discovery-index-client.d.ts b/packages/loopover-miner/lib/discovery-index-client.d.ts deleted file mode 100644 index c0bcbee7f7..0000000000 --- a/packages/loopover-miner/lib/discovery-index-client.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** Opt-in client for the hosted discovery-index service (#4250, #7164/#7166/#7168). Complete no-op unless - * LOOPOVER_MINER_DISCOVERY_PLANE is set -- mirrors sentry.js's own-project opt-in posture: nothing here is ever - * auto-enabled or phones home by default. Supplements (never replaces) opportunity-fanout.js's per-instance - * GitHub fan-out with results from the shared, centrally-cached index, and submits soft-claim coordination at - * work-start/work-end -- the fleet-wide rate-limit mitigation packages/loopover-miner/docs/discovery-plane- - * operator-guide.md documents. Every call here is fail-open (never throws, degrades to "no supplement"/"no - * telemetry sent" on any error), matching orb-export.js's sendAmsExportBatch: an optional-plane hiccup must - * never break the miner's real discover/attempt work. Uses @loopover/engine's discovery-index-contract.ts and - * discovery-soft-claim.ts exports directly rather than re-implementing the shapes. */ -import type { DiscoveryIndexQuery, DiscoveryIndexResponse } from "@loopover/engine"; -export declare const DISCOVERY_PLANE_FLAG = "LOOPOVER_MINER_DISCOVERY_PLANE"; -export declare const DISCOVERY_INDEX_URL_FLAG = "LOOPOVER_MINER_DISCOVERY_INDEX_URL"; -export declare const DISCOVERY_TELEMETRY_FLAG = "LOOPOVER_MINER_DISCOVERY_TELEMETRY"; -export type DiscoveryIndexClientOptions = { - env?: Record; - /** Always called as `fetchImpl(url, init)` with a plain string URL -- narrower than `typeof fetch` on - * purpose, since that's the only shape this module ever actually calls it with. */ - fetchImpl?: (url: string, init: RequestInit) => Promise; - requestTimeoutMs?: number; -}; -/** The shape claim-ledger.js's rowToClaim (and claimIssueWithinCap(...).claim) already produces -- passed - * straight into @loopover/engine's buildSoftClaimRequest with no translation. */ -export type SoftClaimLedgerRecord = { - repoFullName: string; - issueNumber: number; - claimedAt: string; - status: "active" | "released" | "expired"; - note?: string | null; -}; -/** Master opt-in (default off). When false, no discovery-index traffic and no telemetry may be emitted. */ -export declare function isDiscoveryPlaneEnabled(env?: Record): boolean; -/** Second, independent opt-in (default off) for anonymized operational telemetry -- can stay off while the - * plane itself is queried/claimed against. */ -export declare function isDiscoveryTelemetryEnabled(env?: Record): boolean; -/** - * Query the hosted discovery-index for supplementary candidates. Returns `EMPTY_QUERY_RESPONSE` (never throws) - * when the plane is disabled, the URL is unconfigured, or the request fails for any reason -- callers can - * always safely concatenate `.candidates` onto their own locally-discovered set with zero special-casing. - */ -export declare function queryDiscoveryIndex(query: Partial, options?: DiscoveryIndexClientOptions): Promise; -/** - * Best-effort soft-claim submission at work-start/work-end. `claim` is the object returned by claim-ledger.js's - * claimIssueWithinCap(...).claim (or its rowToClaim shape generally) -- passed straight into buildSoftClaimRequest - * with no translation, per that builder's own contract. Fire-and-forget: never throws, and the caller's real - * work (already underway once this is called) is never blocked or aborted by a plane hiccup. Returns - * `{sent: boolean}` for callers that want to log/test the outcome without depending on it for control flow. - */ -export declare function submitSoftClaim(claim: SoftClaimLedgerRecord, options?: DiscoveryIndexClientOptions): Promise<{ - sent: boolean; -}>; -/** - * Emit anonymized, low-cardinality operational telemetry about the plane itself (never per-issue business - * data) -- gated separately behind LOOPOVER_MINER_DISCOVERY_TELEMETRY, per the operator guide's invariant list - * ("low-cardinality reason buckets", mirroring orb-export.js's `reasonBucket` convention). No hosted telemetry - * collector endpoint exists for the discovery plane yet (out of scope for #7164/#7166), so this emits a - * structured local log line via this package's own logger -- the gate, shape, and off-by-default behavior are - * real and tested; swapping in a real remote sink later is a logger-call change, not a design change. - */ -export declare function recordDiscoveryTelemetry(event: string, outcome: string, options?: { - env?: Record; -}): void; diff --git a/packages/loopover-miner/lib/discovery-index-client.js b/packages/loopover-miner/lib/discovery-index-client.js deleted file mode 100644 index 6ce63f78ed..0000000000 --- a/packages/loopover-miner/lib/discovery-index-client.js +++ /dev/null @@ -1,119 +0,0 @@ -import { normalizeDiscoveryIndexRequest, normalizeDiscoveryIndexResponse, buildSoftClaimRequest } from "@loopover/engine"; -import { fetchWithRetry } from "./http-retry.js"; -import { describeCliError } from "./cli-error.js"; -import { getLogger } from "./logger.js"; -export const DISCOVERY_PLANE_FLAG = "LOOPOVER_MINER_DISCOVERY_PLANE"; -export const DISCOVERY_INDEX_URL_FLAG = "LOOPOVER_MINER_DISCOVERY_INDEX_URL"; -export const DISCOVERY_TELEMETRY_FLAG = "LOOPOVER_MINER_DISCOVERY_TELEMETRY"; -const TRUTHY_ENV_VALUE = /^(1|true|yes|on)$/i; -const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; -function isTruthyEnvValue(value) { - return TRUTHY_ENV_VALUE.test(value.trim()); -} -// Reads below use literal `env.LOOPOVER_MINER_*` property access (not the exported *_FLAG constants above, -// which exist for callers/tests to reference the exact name without a typo) because -// scripts/generate-env-reference.mjs statically greps for exactly this `env.NAME ?? "default"` shape to keep -// packages/loopover-miner/docs/env-reference.md honest -- a dynamic `env[SOME_CONST]` lookup is invisible to it. -/** Master opt-in (default off). When false, no discovery-index traffic and no telemetry may be emitted. */ -export function isDiscoveryPlaneEnabled(env = process.env) { - return isTruthyEnvValue(env.LOOPOVER_MINER_DISCOVERY_PLANE ?? ""); -} -/** Second, independent opt-in (default off) for anonymized operational telemetry -- can stay off while the - * plane itself is queried/claimed against. */ -export function isDiscoveryTelemetryEnabled(env = process.env) { - return isTruthyEnvValue(env.LOOPOVER_MINER_DISCOVERY_TELEMETRY ?? ""); -} -function resolveDiscoveryIndexUrl(env) { - const raw = (env.LOOPOVER_MINER_DISCOVERY_INDEX_URL ?? "").trim(); - return raw ? raw.replace(/\/+$/, "") : null; -} -function authHeaders(env) { - const secret = typeof env.LOOPOVER_MINER_DISCOVERY_SHARED_SECRET === "string" ? env.LOOPOVER_MINER_DISCOVERY_SHARED_SECRET.trim() : ""; - return secret ? { authorization: `Bearer ${secret}` } : {}; -} -const EMPTY_QUERY_RESPONSE = Object.freeze({ - contractVersion: 1, - candidates: [], - nextCursor: null, -}); -/** - * Query the hosted discovery-index for supplementary candidates. Returns `EMPTY_QUERY_RESPONSE` (never throws) - * when the plane is disabled, the URL is unconfigured, or the request fails for any reason -- callers can - * always safely concatenate `.candidates` onto their own locally-discovered set with zero special-casing. - */ -export async function queryDiscoveryIndex(query, options = {}) { - const env = options.env ?? process.env; - if (!isDiscoveryPlaneEnabled(env)) - return EMPTY_QUERY_RESPONSE; - const baseUrl = resolveDiscoveryIndexUrl(env); - if (!baseUrl) - return EMPTY_QUERY_RESPONSE; - // fetchWithRetry's signature is deliberately untyped-permissive (`(url: unknown, init?: unknown) => - // Promise`) so it can wrap any fetch-shaped function; the narrower, more useful public - // DiscoveryIndexClientOptions#fetchImpl type is cast at this one boundary rather than widened repo-wide. - const fetchImpl = (options.fetchImpl ?? fetch); - const { request } = normalizeDiscoveryIndexRequest(query); - try { - const response = await fetchWithRetry(fetchImpl, `${baseUrl}/v1/discovery-index/query`, { - method: "POST", - headers: { "content-type": "application/json", ...authHeaders(env) }, - body: JSON.stringify(request.query), - }, { timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS }); - if (!response.ok) - return EMPTY_QUERY_RESPONSE; - const payload = await response.json().catch(() => null); - return normalizeDiscoveryIndexResponse(payload).response; - } - catch { - return EMPTY_QUERY_RESPONSE; - } -} -/** - * Best-effort soft-claim submission at work-start/work-end. `claim` is the object returned by claim-ledger.js's - * claimIssueWithinCap(...).claim (or its rowToClaim shape generally) -- passed straight into buildSoftClaimRequest - * with no translation, per that builder's own contract. Fire-and-forget: never throws, and the caller's real - * work (already underway once this is called) is never blocked or aborted by a plane hiccup. Returns - * `{sent: boolean}` for callers that want to log/test the outcome without depending on it for control flow. - */ -export async function submitSoftClaim(claim, options = {}) { - const env = options.env ?? process.env; - if (!isDiscoveryPlaneEnabled(env)) - return { sent: false }; - const baseUrl = resolveDiscoveryIndexUrl(env); - if (!baseUrl) - return { sent: false }; - const request = buildSoftClaimRequest(claim); - if (request === null) - return { sent: false }; - // fetchWithRetry's signature is deliberately untyped-permissive (`(url: unknown, init?: unknown) => - // Promise`) so it can wrap any fetch-shaped function; the narrower, more useful public - // DiscoveryIndexClientOptions#fetchImpl type is cast at this one boundary rather than widened repo-wide. - const fetchImpl = (options.fetchImpl ?? fetch); - try { - const response = await fetchWithRetry(fetchImpl, `${baseUrl}/v1/discovery-index/soft-claim`, { - method: "POST", - headers: { "content-type": "application/json", ...authHeaders(env) }, - body: JSON.stringify(request), - }, { timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS }); - return { sent: response.ok }; - } - catch (error) { - getLogger().debug("discovery_plane_soft_claim_failed", { error: describeCliError(error) }); - return { sent: false }; - } -} -/** - * Emit anonymized, low-cardinality operational telemetry about the plane itself (never per-issue business - * data) -- gated separately behind LOOPOVER_MINER_DISCOVERY_TELEMETRY, per the operator guide's invariant list - * ("low-cardinality reason buckets", mirroring orb-export.js's `reasonBucket` convention). No hosted telemetry - * collector endpoint exists for the discovery plane yet (out of scope for #7164/#7166), so this emits a - * structured local log line via this package's own logger -- the gate, shape, and off-by-default behavior are - * real and tested; swapping in a real remote sink later is a logger-call change, not a design change. - */ -export function recordDiscoveryTelemetry(event, outcome, options = {}) { - const env = options.env ?? process.env; - if (!isDiscoveryPlaneEnabled(env) || !isDiscoveryTelemetryEnabled(env)) - return; - getLogger().info("discovery_plane_telemetry", { event, outcome }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY292ZXJ5LWluZGV4LWNsaWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvdmVyeS1pbmRleC1jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBVUEsT0FBTyxFQUFFLDhCQUE4QixFQUFFLCtCQUErQixFQUFFLHFCQUFxQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDMUgsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2xELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFeEMsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsZ0NBQWdDLENBQUM7QUFDckUsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsb0NBQW9DLENBQUM7QUFDN0UsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsb0NBQW9DLENBQUM7QUFvQjdFLE1BQU0sZ0JBQWdCLEdBQUcsb0JBQW9CLENBQUM7QUFDOUMsTUFBTSwwQkFBMEIsR0FBRyxNQUFNLENBQUM7QUFFMUMsU0FBUyxnQkFBZ0IsQ0FBQyxLQUFhO0lBQ3JDLE9BQU8sZ0JBQWdCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQzdDLENBQUM7QUFFRCwyR0FBMkc7QUFDM0csb0ZBQW9GO0FBQ3BGLDZHQUE2RztBQUM3RyxpSEFBaUg7QUFFakgsMkdBQTJHO0FBQzNHLE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMzRixPQUFPLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyw4QkFBOEIsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUNwRSxDQUFDO0FBRUQ7K0NBQytDO0FBQy9DLE1BQU0sVUFBVSwyQkFBMkIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMvRixPQUFPLGdCQUFnQixDQUFDLEdB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/discovery-throttle.d.ts b/packages/loopover-miner/lib/discovery-throttle.d.ts deleted file mode 100644 index 842b45dedd..0000000000 --- a/packages/loopover-miner/lib/discovery-throttle.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** At or below this remaining budget, serialize discovery to a single in-flight request. */ -export declare const DEFAULT_RATE_LIMIT_LOW_WATER_MARK = 50; -/** At or above this remaining budget, run at the full configured concurrency. */ -export declare const DEFAULT_RATE_LIMIT_HIGH_WATER_MARK = 250; -/** - * Resolve the concurrency the fanout may run at for the currently-recorded rate-limit budget. Returns an integer - * in `[1, baseConcurrency]`: - * - an unknown budget (`null`/non-finite — nothing recorded yet) runs at full `baseConcurrency`; - * - at or below `lowWaterMark` it clamps to a single in-flight request; - * - at or above `highWaterMark` it runs at full `baseConcurrency`; - * - in between it scales linearly with the remaining fraction of the low→high band. - */ -export declare function resolveThrottledConcurrency(baseConcurrency: number, rateLimitRemaining: number | null, lowWaterMark: number, highWaterMark: number): number; diff --git a/packages/loopover-miner/lib/discovery-throttle.js b/packages/loopover-miner/lib/discovery-throttle.js deleted file mode 100644 index 05be67166e..0000000000 --- a/packages/loopover-miner/lib/discovery-throttle.js +++ /dev/null @@ -1,30 +0,0 @@ -// Dynamic discovery back-off (#4844): the fanout already records GitHub's `x-ratelimit-remaining`, but nothing -// slowed its own concurrent fetching in response — a `discover` run could sprint at full concurrency straight -// into a 403. This pure helper maps the recorded remaining budget to an allowed in-flight concurrency so the -// fanout tapers off as the budget approaches zero. It only decides *how many* requests may run; it never changes -// which docs are fetched or how a policy verdict is derived from them. -/** At or below this remaining budget, serialize discovery to a single in-flight request. */ -export const DEFAULT_RATE_LIMIT_LOW_WATER_MARK = 50; -/** At or above this remaining budget, run at the full configured concurrency. */ -export const DEFAULT_RATE_LIMIT_HIGH_WATER_MARK = 250; -/** - * Resolve the concurrency the fanout may run at for the currently-recorded rate-limit budget. Returns an integer - * in `[1, baseConcurrency]`: - * - an unknown budget (`null`/non-finite — nothing recorded yet) runs at full `baseConcurrency`; - * - at or below `lowWaterMark` it clamps to a single in-flight request; - * - at or above `highWaterMark` it runs at full `baseConcurrency`; - * - in between it scales linearly with the remaining fraction of the low→high band. - */ -export function resolveThrottledConcurrency(baseConcurrency, rateLimitRemaining, lowWaterMark, highWaterMark) { - if (!Number.isFinite(rateLimitRemaining)) - return baseConcurrency; - if (rateLimitRemaining <= lowWaterMark) - return 1; - if (rateLimitRemaining >= highWaterMark) - return baseConcurrency; - // remaining is strictly inside the (low, high) band, so the fraction is in (0, 1) and the ceil lands in - // [1, baseConcurrency] without any further clamping. - const fraction = (rateLimitRemaining - lowWaterMark) / (highWaterMark - lowWaterMark); - return Math.ceil(fraction * baseConcurrency); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY292ZXJ5LXRocm90dGxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGlzY292ZXJ5LXRocm90dGxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtHQUErRztBQUMvRyw4R0FBOEc7QUFDOUcsNkdBQTZHO0FBQzdHLGlIQUFpSDtBQUNqSCx1RUFBdUU7QUFFdkUsNEZBQTRGO0FBQzVGLE1BQU0sQ0FBQyxNQUFNLGlDQUFpQyxHQUFHLEVBQUUsQ0FBQztBQUNwRCxpRkFBaUY7QUFDakYsTUFBTSxDQUFDLE1BQU0sa0NBQWtDLEdBQUcsR0FBRyxDQUFDO0FBRXREOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsMkJBQTJCLENBQ3pDLGVBQXVCLEVBQ3ZCLGtCQUFpQyxFQUNqQyxZQUFvQixFQUNwQixhQUFxQjtJQUVyQixJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsQ0FBQztRQUFFLE9BQU8sZUFBZSxDQUFDO0lBQ2pFLElBQUksa0JBQW1CLElBQUksWUFBWTtRQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ2xELElBQUksa0JBQW1CLElBQUksYUFBYTtRQUFFLE9BQU8sZUFBZSxDQUFDO0lBQ2pFLHdHQUF3RztJQUN4RyxxREFBcUQ7SUFDckQsTUFBTSxRQUFRLEdBQUcsQ0FBQyxrQkFBbUIsR0FBRyxZQUFZLENBQUMsR0FBRyxDQUFDLGFBQWEsR0FBRyxZQUFZLENBQUMsQ0FBQztJQUN2RixPQUFPLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxHQUFHLGVBQWUsQ0FBQyxDQUFDO0FBQy9DLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/env-file-indirection.d.ts b/packages/loopover-miner/lib/env-file-indirection.d.ts deleted file mode 100644 index c970aefbde..0000000000 --- a/packages/loopover-miner/lib/env-file-indirection.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Scan `env` for `_FILE` vars and resolve each into `` in place, reading the referenced file's - * contents (trimmed). An explicit `` value always wins over `_FILE` (mirrors the ORB analogue's - * precedence rule exactly) -- a `_FILE` var is only consulted when its plain counterpart is unset. Throws a - * clear, actionable error identifying the offending `_FILE` var and its file path when the file is - * missing or unreadable -- this never silently leaves a credential empty/undefined. Never logs or returns any - * resolved secret value itself; only the (non-secret) var name and file path ever appear in a thrown message. - * - * `env` and `readFile` are injectable purely for testability -- every real caller uses the defaults - * (`process.env`, `node:fs`'s `readFileSync`), so this is byte-identical to a hardcoded version at runtime. - */ -export declare function loadMinerFileSecrets(env?: Record, readFile?: (path: string) => string): void; diff --git a/packages/loopover-miner/lib/env-file-indirection.js b/packages/loopover-miner/lib/env-file-indirection.js deleted file mode 100644 index d4ff5d04ae..0000000000 --- a/packages/loopover-miner/lib/env-file-indirection.js +++ /dev/null @@ -1,42 +0,0 @@ -// Resolve `_FILE` env vars (Docker/Swarm/K8s secret mounts) into `` at miner startup (#5178). -// Ports src/selfhost/load-file-secrets.ts's pattern into the miner package -- the miner is a separate -// deployable (its own process/container per DEPLOYMENT.md's fleet mode), so it never runs through ORB's own -// server-startup resolver. Both modules agree on failure handling: an unreadable/missing `_FILE` -// THROWS. The ORB analogue logs a `selfhost_secret_file_unreadable` line first and then throws the same -// way -- it converged onto this fail-fast behavior in #6284, so the divergence this comment used to -// describe no longer exists (pinned by test/unit/env-file-indirection-parity.test.ts). A broken secret -// mount therefore fails a container fast and loud, never silently proceeding with an unset/empty -// credential the next real GitHub call would then fail on anyway, with a far less specific error. -import { readFileSync } from "node:fs"; -// Docker Compose's OWN reserved `_FILE`-suffixed environment variables -- never loopover's secret-file -// convention, so they must never be dereferenced below (mirrors src/selfhost/load-file-secrets.ts's own -// exclusion and rationale: `COMPOSE_FILE` is a colon-delimited list of compose file paths, never a single -// readable file itself, and `COMPOSE_ENV_FILE` points at an operator's own .env file, not a secret). -const COMPOSE_RESERVED_FILE_VARS = new Set(["COMPOSE_FILE", "COMPOSE_ENV_FILE"]); -/** - * Scan `env` for `_FILE` vars and resolve each into `` in place, reading the referenced file's - * contents (trimmed). An explicit `` value always wins over `_FILE` (mirrors the ORB analogue's - * precedence rule exactly) -- a `_FILE` var is only consulted when its plain counterpart is unset. Throws a - * clear, actionable error identifying the offending `_FILE` var and its file path when the file is - * missing or unreadable -- this never silently leaves a credential empty/undefined. Never logs or returns any - * resolved secret value itself; only the (non-secret) var name and file path ever appear in a thrown message. - * - * `env` and `readFile` are injectable purely for testability -- every real caller uses the defaults - * (`process.env`, `node:fs`'s `readFileSync`), so this is byte-identical to a hardcoded version at runtime. - */ -export function loadMinerFileSecrets(env = process.env, readFile = (path) => readFileSync(path, "utf8")) { - for (const key of Object.keys(env)) { - if (!key.endsWith("_FILE") || !env[key] || COMPOSE_RESERVED_FILE_VARS.has(key)) - continue; - const target = key.slice(0, -"_FILE".length); - if (env[target]) - continue; // an explicit value always wins over _FILE - try { - env[target] = readFile(env[key]).trim(); - } - catch (error) { - throw new Error(`Failed to read secret file for ${key} (${env[key]}): ${error instanceof Error ? error.message : String(error)}`); - } - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LWZpbGUtaW5kaXJlY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJlbnYtZmlsZS1pbmRpcmVjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwwR0FBMEc7QUFDMUcsc0dBQXNHO0FBQ3RHLDRHQUE0RztBQUM1Ryx1R0FBdUc7QUFDdkcsd0dBQXdHO0FBQ3hHLG9HQUFvRztBQUNwRyx1R0FBdUc7QUFDdkcsaUdBQWlHO0FBQ2pHLGtHQUFrRztBQUNsRyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBRXZDLHVHQUF1RztBQUN2Ryx3R0FBd0c7QUFDeEcsMEdBQTBHO0FBQzFHLHFHQUFxRztBQUNyRyxNQUFNLDBCQUEwQixHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsY0FBYyxFQUFFLGtCQUFrQixDQUFDLENBQUMsQ0FBQztBQUVqRjs7Ozs7Ozs7OztHQVVHO0FBQ0gsTUFBTSxVQUFVLG9CQUFvQixDQUNsQyxNQUEwQyxPQUFPLENBQUMsR0FBRyxFQUNyRCxXQUFxQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUM7SUFFekUsS0FBSyxNQUFNLEdBQUcsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDbkMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLElBQUksMEJBQTBCLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQztZQUFFLFNBQVM7UUFDekYsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDN0MsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDO1lBQUUsU0FBUyxDQUFDLHdEQUF3RDtRQUNuRixJQUFJLENBQUM7WUFDSCxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsUUFBUSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO1FBQzFDLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsTUFBTSxJQUFJLEtBQUssQ0FDYixrQ0FBa0MsR0FBRyxLQUFLLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFDaEQsS0FBSyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FDdkQsRUFBRSxDQUNILENBQUM7UUFDSixDQUFDO0lBQ0gsQ0FBQztBQUNILENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/event-ledger-cli.d.ts b/packages/loopover-miner/lib/event-ledger-cli.d.ts deleted file mode 100644 index 2c5e98fcfa..0000000000 --- a/packages/loopover-miner/lib/event-ledger-cli.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { EventLedger, LedgerEntry } from "./event-ledger.js"; -export type ParsedLedgerListArgs = { - json: boolean; - repoFullName: string | null; - since: number | null; - type: string | null; -} | { - error: string; -}; -export type EventLedgerCliOptions = { - initEventLedger?: () => EventLedger; -}; -export declare function parseLedgerListArgs(args: string[]): ParsedLedgerListArgs; -export declare function filterLedgerEvents(events: LedgerEntry[], options?: { - type?: string | null; -}): LedgerEntry[]; -/** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */ -export declare const AUDIT_FEED_ENTRY_FIELDS: readonly ["eventType", "repoFullName", "outcome", "actor", "detail", "createdAt"]; -type AuditFeedEntry = { - eventType: string; - repoFullName: string | null; - outcome: string | null; - actor: string | null; - detail: string | null; - createdAt: string; -}; -/** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */ -export declare function projectLedgerEventToAuditFeedEntry(entry: LedgerEntry): AuditFeedEntry; -export type AuditFeedMcpFilterInput = { - repoFullName?: string | null; - since?: number | null; - type?: string | null; -}; -type NormalizedAuditFeedFilter = { - repoFullName: string | null; - since: number | null; - type: string | null; -}; -/** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */ -export declare function normalizeAuditFeedMcpFilter(input?: AuditFeedMcpFilterInput): NormalizedAuditFeedFilter; -/** Read-only audit feed shared by the MCP audit-feed tool (#5158). */ -export declare function collectEventLedgerAuditFeed(eventLedger: EventLedger, filter?: { - repoFullName?: string | null; - since?: number | null; - type?: string | null; -}): { - repoFullName?: string; - events: AuditFeedEntry[]; -}; -export declare function renderLedgerTable(events: LedgerEntry[]): string; -/** - * Render event-ledger activity as Prometheus text-exposition counters: one `loopover_miner_events_total{type}` - * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling - * `ledger list --json` (#4841). Pure + side-effect-free — the caller supplies the rows and prints the result; - * deterministic (series emitted in sorted type order); always emits HELP/TYPE so an empty ledger is still a - * well-formed exposition document. - */ -export declare function renderEventLedgerMetrics(events: readonly LedgerEntry[]): string; -export declare function runLedgerList(args: string[], options?: EventLedgerCliOptions): number; -export declare function runLedgerMetrics(args: string[], options?: EventLedgerCliOptions): number; -export declare function runLedgerCli(subcommand: string | undefined, args: string[], options?: EventLedgerCliOptions): number; -export {}; diff --git a/packages/loopover-miner/lib/event-ledger-cli.js b/packages/loopover-miner/lib/event-ledger-cli.js deleted file mode 100644 index 222242ebd6..0000000000 --- a/packages/loopover-miner/lib/event-ledger-cli.js +++ /dev/null @@ -1,261 +0,0 @@ -import { initEventLedger } from "./event-ledger.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -const LEDGER_LIST_USAGE = "Usage: loopover-miner ledger list [--repo ] [--since ] [--type ] [--json]"; -function parseRepoArg(value, usage) { - if (!value) - return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -function parseSinceArg(value) { - const since = Number(value); - if (!Number.isInteger(since) || since < 0) { - return { error: "since must be a non-negative integer seq cursor." }; - } - return { since }; -} -export function parseLedgerListArgs(args) { - const options = { - json: false, - repoFullName: null, - since: null, - type: null, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) - return { error: LEDGER_LIST_USAGE }; - const repo = parseRepoArg(repoArg, LEDGER_LIST_USAGE); - if ("error" in repo) - return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token === "--since") { - const sinceArg = args[index + 1]; - if (!sinceArg || sinceArg.startsWith("--")) - return { error: LEDGER_LIST_USAGE }; - const parsedSince = parseSinceArg(sinceArg); - if ("error" in parsedSince) - return parsedSince; - options.since = parsedSince.since; - index += 1; - continue; - } - if (token === "--type") { - const type = args[index + 1]; - if (!type || type.startsWith("-")) - return { error: LEDGER_LIST_USAGE }; - options.type = type.trim(); - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - positional.push(token); - } - if (positional.length > 0) - return { error: LEDGER_LIST_USAGE }; - return options; -} -export function filterLedgerEvents(events, options = {}) { - if (!Array.isArray(events)) - return []; - const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; - if (!type) - return events; - return events.filter((entry) => entry.type === type); -} -/** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */ -export const AUDIT_FEED_ENTRY_FIELDS = Object.freeze([ - "eventType", - "repoFullName", - "outcome", - "actor", - "detail", - "createdAt", -]); -function optionalMetadataString(value) { - if (typeof value !== "string") - return null; - const trimmed = value.trim(); - return trimmed || null; -} -/** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */ -export function projectLedgerEventToAuditFeedEntry(entry) { - const payload = entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {}; - return { - eventType: entry.type, - repoFullName: entry.repoFullName, - outcome: optionalMetadataString(payload.outcome), - actor: optionalMetadataString(payload.actor), - detail: optionalMetadataString(payload.detail), - createdAt: entry.createdAt, - }; -} -/** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */ -export function normalizeAuditFeedMcpFilter(input = {}) { - if (input === null || typeof input !== "object" || Array.isArray(input)) { - throw new Error("filter must be an object"); - } - const filter = { repoFullName: null, since: null, type: null }; - if (input.repoFullName !== undefined && input.repoFullName !== null) { - const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form."); - if ("error" in repo) - throw new Error(repo.error); - filter.repoFullName = repo.repoFullName; - } - if (input.since !== undefined && input.since !== null) { - const parsedSince = parseSinceArg(String(input.since)); - if ("error" in parsedSince) - throw new Error(parsedSince.error); - filter.since = parsedSince.since; - } - if (input.type !== undefined && input.type !== null) { - const trimmed = String(input.type).trim(); - if (!trimmed) - throw new Error("type must be a non-empty string."); - filter.type = trimmed; - } - return filter; -} -/** Read-only audit feed shared by the MCP audit-feed tool (#5158). */ -export function collectEventLedgerAuditFeed(eventLedger, filter = {}) { - const events = filterLedgerEvents(eventLedger.readEvents({ - ...(filter.repoFullName !== undefined ? { repoFullName: filter.repoFullName } : {}), - ...(filter.since !== undefined ? { since: filter.since } : {}), - }), { ...(filter.type !== undefined ? { type: filter.type } : {}) }); - return { - ...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}), - events: events.map(projectLedgerEventToAuditFeedEntry), - }; -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderLedgerTable(events) { - if (!Array.isArray(events) || events.length === 0) - return "no event ledger entries"; - const header = [ - "seq".padStart(4), - "type".padEnd(20), - "repo".padEnd(24), - "created-at".padEnd(24), - ].join(" "); - const lines = events.map((entry) => [ - String(entry.seq).padStart(4), - entry.type.padEnd(20), - display(entry.repoFullName).padEnd(24), - display(entry.createdAt).padEnd(24), - ].join(" ")); - return [header, ...lines].join("\n"); -} -const EVENT_LEDGER_METRICS_USAGE = "Usage: loopover-miner ledger metrics"; -// Prometheus metric name for the per-type event-ledger counter. Mirrors the `loopover_miner_*_total` naming and -// the HELP/TYPE/label conventions of the engine's renderMinerPredictionMetrics -// (packages/loopover-engine/src/miner-prediction-metrics.ts) rather than importing across the package boundary. -const MINER_EVENTS_TOTAL = "loopover_miner_events_total"; -/** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */ -function escapeHelpText(help) { - return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); -} -/** Prometheus label-value escaping — backslash, double-quote, newline — so an arbitrary event `type` string can - * never break the metric line (mirrors miner-prediction-metrics.ts's escapeLabelValue). */ -function escapeLabelValue(value) { - return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); -} -/** - * Render event-ledger activity as Prometheus text-exposition counters: one `loopover_miner_events_total{type}` - * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling - * `ledger list --json` (#4841). Pure + side-effect-free — the caller supplies the rows and prints the result; - * deterministic (series emitted in sorted type order); always emits HELP/TYPE so an empty ledger is still a - * well-formed exposition document. - */ -export function renderEventLedgerMetrics(events) { - const totalByType = new Map(); - for (const entry of events) { - totalByType.set(entry.type, (totalByType.get(entry.type) ?? 0) + 1); - } - const lines = [ - `# HELP ${MINER_EVENTS_TOTAL} ${escapeHelpText("Event-ledger entries the miner has recorded, by event type.")}`, - `# TYPE ${MINER_EVENTS_TOTAL} counter`, - ]; - for (const [type, count] of [...totalByType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { - lines.push(`${MINER_EVENTS_TOTAL}{type="${escapeLabelValue(type)}"} ${count}`); - } - return `${lines.join("\n")}\n`; -} -function withEventLedger(options, run) { - const ownsLedger = options.initEventLedger === undefined; - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - try { - return run(eventLedger); - } - finally { - if (ownsLedger) - eventLedger.close(); - } -} -export function runLedgerList(args, options = {}) { - const parsed = parseLedgerListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return withEventLedger(options, (eventLedger) => { - const events = filterLedgerEvents(eventLedger.readEvents({ - repoFullName: parsed.repoFullName, - since: parsed.since, - }), { type: parsed.type }); - if (parsed.json) { - console.log(JSON.stringify({ events }, null, 2)); - } - else { - console.log(renderLedgerTable(events)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runLedgerMetrics(args, options = {}) { - if (args.length > 0) { - return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE); - } - try { - return withEventLedger(options, (eventLedger) => { - // renderEventLedgerMetrics returns a newline-terminated document; console.log re-adds the terminator, so - // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics). - console.log(renderEventLedgerMetrics(eventLedger.readEvents()).trimEnd()); - return 0; - }); - } - catch (error) { - return reportCliFailure(argsWantJson(args), describeCliError(error)); - } -} -export function runLedgerCli(subcommand, args, options = {}) { - if (subcommand === "list") - return runLedgerList(args, options); - if (subcommand === "metrics") - return runLedgerMetrics(args, options); - return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXZlbnQtbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImV2ZW50LWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXBELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUVyRCxNQUFNLGlCQUFpQixHQUNyQix1R0FBdUcsQ0FBQztBQWdCMUcsU0FBUyxZQUFZLENBQUMsS0FBeUIsRUFBRSxLQUFhO0lBQzVELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsQ0FBQztJQUNwQyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDN0IsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNoRCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLGtCQUFrQixDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDdEcsT0FBTyxFQUFFLEtBQUssRUFBRSx3Q0FBd0MsRUFBRSxDQUFDO0lBQzdELENBQUM7SUFDRCxPQUFPLEVBQUUsWUFBWSxFQUFFLEdBQUcsS0FBSyxJQUFJLElBQUksRUFBRSxFQUFFLENBQUM7QUFDOUMsQ0FBQztBQUVELFNBQVMsYUFBYSxDQUFDLEtBQWE7SUFDbEMsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQzVCLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEtB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/event-ledger.d.ts b/packages/loopover-miner/lib/event-ledger.d.ts deleted file mode 100644 index e75b19aede..0000000000 --- a/packages/loopover-miner/lib/event-ledger.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type LedgerEntry = { - id: number; - seq: number; - type: string; - repoFullName: string | null; - payload: Record; - createdAt: string; -}; -export type AppendEventInput = { - type: string; - repoFullName?: string; - payload: Record; -}; -export type ReadEventsFilter = { - repoFullName?: string | null; - since?: number | null; -}; -export type EventLedger = { - dbPath: string; - appendEvent(event: AppendEventInput): LedgerEntry; - readEvents(filter?: ReadEventsFilter): LedgerEntry[]; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare function resolveEventLedgerDbPath(env?: Record): string; -/** - * Opens the local append-only event ledger, creating the table on first use. `seq` is a monotonically increasing - * counter maintained by this module (next = current MAX(seq) + 1) rather than relying on `AUTOINCREMENT`'s - * reuse-after-vacuum behavior, so consumers get a stable ordering guarantee. Rows read back in `seq ASC` order. - * (#2290) - */ -export declare function initEventLedger(dbPath?: string): EventLedger; -export declare function appendEvent(event: AppendEventInput): LedgerEntry; -export declare function readEvents(filter?: ReadEventsFilter): LedgerEntry[]; -export declare function closeDefaultEventLedger(): void; diff --git a/packages/loopover-miner/lib/event-ledger.js b/packages/loopover-miner/lib/event-ledger.js deleted file mode 100644 index 7b98e2e686..0000000000 --- a/packages/loopover-miner/lib/event-ledger.js +++ /dev/null @@ -1,201 +0,0 @@ -import { isDeepStrictEqual } from "node:util"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { EVENT_LEDGER_PURGE_SPEC, EVENT_LEDGER_RETENTION_SPEC, purgeStoreByRepo, pruneLedgerByRetention, resolveLedgerRetentionPolicy, } from "./store-maintenance.js"; -const defaultDbFileName = "event-ledger.sqlite3"; -let defaultEventLedger = null; -export function resolveEventLedgerDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_EVENT_LEDGER_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveEventLedgerDbPath(), "invalid_event_ledger_db_path"); -} -function normalizeEventType(type) { - if (typeof type !== "string") - throw new Error("invalid_event_type"); - const trimmed = type.trim(); - if (!trimmed) - throw new Error("invalid_event_type"); - return trimmed; -} -/** Optional repo scope: omitted/nullish → null; otherwise a validated `owner/repo`. */ -function normalizeOptionalRepoFullName(repoFullName) { - if (repoFullName === undefined || repoFullName === null) - return null; - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -/** Optional seq cursor for polling: omitted → undefined; otherwise a non-negative integer last-seen seq. */ -function normalizeOptionalSince(since) { - if (since === undefined || since === null) - return undefined; - if (typeof since !== "number" || !Number.isInteger(since) || since < 0) { - throw new Error("invalid_since"); - } - return since; -} -/** Read-filter repo scope: omitted/nullish → unscoped (all events); otherwise a validated `owner/repo`. */ -function normalizeReadRepoFilter(repoFullName) { - if (repoFullName === undefined || repoFullName === null) - return undefined; - return normalizeOptionalRepoFullName(repoFullName); -} -// Serialize an audit payload, enforcing that it round-trips through JSON VERBATIM. A plain JSON.stringify would -// silently drop `undefined`/function/symbol values and coerce `NaN`/`Infinity` to `null` (and throw on BigInt or a -// cycle), so a read-back would not equal the appended event. We reject any such lossy payload outright — an audit -// ledger must return exactly what was recorded. -function serializePayload(payload) { - if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { - throw new Error("invalid_payload"); - } - let json; - try { - json = JSON.stringify(payload); - } - catch { - throw new Error("invalid_payload"); // BigInt value or circular reference - } - if (!isDeepStrictEqual(JSON.parse(json), payload)) { - throw new Error("invalid_payload"); // a value JSON would drop or coerce (undefined/NaN/function/symbol/Date/…) - } - return json; -} -function rowToEntry(row) { - return { - id: row.id, - seq: row.seq, - type: row.event_type, - repoFullName: row.repo_full_name, - payload: JSON.parse(row.payload_json), - createdAt: row.created_at, - }; -} -function asEventDbRow(row) { - return row; -} -// v1 -> v2 (#4939): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of this -// same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing reads or -// writes it yet (no consumer exists until a future hosted deployment populates it). Same defensive -// column-presence guard as this file's sibling stores' own additive migrations (e.g. portfolio-queue.js's -// leased_at addition). -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(miner_event_ledger)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE miner_event_ledger ADD COLUMN tenant_id TEXT"); -} -/** - * Opens the local append-only event ledger, creating the table on first use. `seq` is a monotonically increasing - * counter maintained by this module (next = current MAX(seq) + 1) rather than relying on `AUTOINCREMENT`'s - * reuse-after-vacuum behavior, so consumers get a stable ordering guarantee. Rows read back in `seq ASC` order. - * (#2290) - */ -export function initEventLedger(dbPath = resolveEventLedgerDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - // `UNIQUE(seq)` makes the monotonic-ordering guarantee an enforced invariant: a duplicate seq can never persist, - // even if the append path were ever changed. - db.exec(` - CREATE TABLE IF NOT EXISTS miner_event_ledger ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - seq INTEGER NOT NULL UNIQUE, - event_type TEXT NOT NULL, - repo_full_name TEXT, - payload_json TEXT NOT NULL, - created_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addTenantIdColumn]); - // Opt-in retention (#4834): prune aged/excess rows when an operator has enabled it; a no-op by default. - pruneLedgerByRetention(db, EVENT_LEDGER_RETENTION_SPEC, resolveLedgerRetentionPolicy(), Date.now()); - const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM miner_event_ledger"); - const appendStatement = db.prepare(` - INSERT INTO miner_event_ledger (seq, event_type, repo_full_name, payload_json, created_at) - VALUES (?, ?, ?, ?, ?) - `); - const getByIdStatement = db.prepare("SELECT * FROM miner_event_ledger WHERE id = ?"); - const readAllStatement = db.prepare("SELECT * FROM miner_event_ledger ORDER BY seq ASC"); - const readByRepoStatement = db.prepare("SELECT * FROM miner_event_ledger WHERE repo_full_name = ? ORDER BY seq ASC"); - const readSinceStatement = db.prepare("SELECT * FROM miner_event_ledger WHERE seq > ? ORDER BY seq ASC"); - const readByRepoSinceStatement = db.prepare("SELECT * FROM miner_event_ledger WHERE repo_full_name = ? AND seq > ? ORDER BY seq ASC"); - return { - dbPath: resolvedPath, - appendEvent(event) { - const type = normalizeEventType(event?.type); - const repoFullName = normalizeOptionalRepoFullName(event?.repoFullName); - const payloadJson = serializePayload(event?.payload); - const createdAt = new Date().toISOString(); - // Serialize the read-then-write: BEGIN IMMEDIATE takes the write lock BEFORE reading MAX(seq), so two ledger - // instances on the same file cannot both compute the same next seq and corrupt the ordering guarantee. - db.exec("BEGIN IMMEDIATE"); - try { - const { nextSeq } = nextSeqStatement.get(); - const result = appendStatement.run(nextSeq, type, repoFullName, payloadJson, createdAt); - const entry = rowToEntry(asEventDbRow(getByIdStatement.get(Number(result.lastInsertRowid)))); - db.exec("COMMIT"); - return entry; - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - }, - readEvents(filter = {}) { - const repoFullName = normalizeReadRepoFilter(filter.repoFullName); - // `since` returns events with a seq STRICTLY greater than it — the "give me everything after the last seq I - // saw" polling shape. - const since = normalizeOptionalSince(filter.since); - let rows; - if (repoFullName !== undefined && since !== undefined) { - rows = readByRepoSinceStatement.all(repoFullName, since); - } - else if (repoFullName !== undefined) { - rows = readByRepoStatement.all(repoFullName); - } - else if (since !== undefined) { - rows = readSinceStatement.all(since); - } - else { - rows = readAllStatement.all(); - } - return rows.map((row) => rowToEntry(asEventDbRow(row))); - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. See the - // IMMUTABILITY INVARIANT note above: this is a deliberate, separate exception, not a normal ledger write. - // Requires a real repoFullName (unlike the optional filter above): a purge must never silently no-op on a - // missing/blank argument. - purgeByRepo(repoFullName) { - const normalized = normalizeOptionalRepoFullName(repoFullName); - if (normalized === null) - throw new Error("invalid_repo_full_name"); - return purgeStoreByRepo(db, EVENT_LEDGER_PURGE_SPEC, normalized); - }, - close() { - db.close(); - }, - }; -} -function getDefaultEventLedger() { - defaultEventLedger ??= initEventLedger(); - return defaultEventLedger; -} -export function appendEvent(event) { - return getDefaultEventLedger().appendEvent(event); -} -export function readEvents(filter) { - return getDefaultEventLedger().readEvents(filter); -} -export function closeDefaultEventLedger() { - if (!defaultEventLedger) - return; - defaultEventLedger.close(); - defaultEventLedger = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXZlbnQtbGVkZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZXZlbnQtbGVkZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUM5QyxPQUFPLEVBQUUseUJBQXlCLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN4RyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUM1RCxPQUFPLEVBQ0wsdUJBQXVCLEVBQ3ZCLDJCQUEyQixFQUMzQixnQkFBZ0IsRUFDaEIsc0JBQXNCLEVBQ3RCLDRCQUE0QixHQUM3QixNQUFNLHdCQUF3QixDQUFDO0FBbURoQyxNQUFNLGlCQUFpQixHQUFHLHNCQUFzQixDQUFDO0FBQ2pELElBQUksa0JBQWtCLEdBQXVCLElBQUksQ0FBQztBQUVsRCxNQUFNLFVBQVUsd0JBQXdCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDNUYsT0FBTyx1QkFBdUIsQ0FBQyxpQkFBaUIsRUFBRSxnQ0FBZ0MsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUMzRixDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsTUFBaUM7SUFDeEQsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsd0JBQXdCLEVBQUUsRUFBRSw4QkFBOEIsQ0FBQyxDQUFDO0FBQ3ZHLENBQUM7QUFFRCxTQUFTLGtCQUFrQixDQUFDLElBQWE7SUFDdkMsSUFBSSxPQUFPLElBQUksS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0lBQ3BFLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM1QixJQUFJLENBQUMsT0FBTztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsb0JBQW9CLENBQUMsQ0FBQztJQUNwRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQsdUZBQXVGO0FBQ3ZGLFNBQVMsNkJBQTZCLENBQUMsWUFBcUI7SUFDMUQsSUFBSSxZQUFZLEtBQUssU0FBUyxJQUFJLFlBQVksS0FBSyxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDckUsSUFBSSxPQUFPLFlBQVksS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ2hGLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDNUQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUN0RixPQUFPLEdBQUcsS0FBSyxJQUFJLElBQUksRUFBRSxDQUFDO0FBQzVCLENBQUM7QUFFRCw0R0FBNEc7QUFDNUcsU0FBUyxzQkFBc0IsQ0FBQyxLQUFjO0lBQzVDLElBQUksS0FBSyxLQUFLLFNBQVMsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQzVELElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDdkUsTUFBTSxJQUFJLEtBQUssQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUNuQyxDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUM7QUFDZixDQUFDO0FBRUQsMkdBQTJHO0FBQzNHLFNBQVMsdUJBQXVCLENBQUMsWUFBcUI7SUFDcEQsSUFBSSxZQUFZLEtBQUssU0FBUyxJQUFJLFlBQVksS0FBSyxJQUFJO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDMUUsT0FBTyw2QkFBNkIsQ0FBQyxZQUFZLENBQUMsQ0FBQztBQUNyRCxDQUFDO0FBRUQsZ0hBQWdIO0FBQ2hILG1IQUFtSDtBQUNuSCxrSEFBa0g7QUFDbEgsZ0RBQWdEO0FBQ2hELFNBQVMsZ0JBQWdCLENBQUMsT0FBZ0I7SUFDeEMsSUFBSSxPQUFPLEtBQUssSUFBSSxJQUFJLE9BQU8sT0FBTyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDOUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ3JDLENBQUM7SUFDRCxJQUFJLElBQVksQ0FBQztJQUNqQixJQUFJLENBQUM7UUFDSCxJQUFJLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNqQyxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMscUNBQXFDO0lBQzNFLENBQUM7SUFDRCxJQUFJLENBQUMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxPQUFPLENBQUMsRUFBRSxDQUFDO1FBQ2xELE1BQU0sSUFBSSxLQUFLLENBQUMsaUJBQWlCLENBQUMsQ0FBQyxDQUFDLDJFQUEyRTtJQUNqSCxDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBRUQsU0FBUyxVQUFVLENBQUMsR0FBZTtJQUNqQyxPQUFPO1FBQ0wsRUFBRSxFQUFFLEdBQUcsQ0FBQyxFQUFFO1FBQ1YsR0FBRyxFQUFFLEdBQUcsQ0FBQyxHQUFHO1FBQ1osSUFBSSxFQUFFLEdBQUcsQ0FBQyxVQUFVO1FBQ3BCLFlBQVksRUFBRSxHQUFHLENBQUMsY0FBYztRQUNoQyxPQUFPLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDO1FBQ3JDLFNBQVMsRUFBRSxHQUFHLENBQUMsVUFBVTtLQUMxQixDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEdBQW1DO0lBQ3ZELE9BQU8sR0FBNEIsQ0FBQztBQUN0QyxDQUFDO0FBRUQsNEdBQTRHO0FBQzVHLCtHQUErRztBQUMvRyxtR0FBbUc7QUFDbkcsMEdBQTBHO0FBQzFHLHVCQUF1QjtBQUN2QixTQUFTLGlCQUFpQixDQUFDLEVBQWdCO0lBQ3pDLE1BQU0saUJBQWlCLEdBQUcsRUFBRTtTQUN6QixPQUFPLENBQUMsdUNBQXVDLENBQUM7U0FDaEQsR0FBRyxFQUFFO1NBQ0wsSUFBSSxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxLQUFLLFdBQVcsQ0FBQyxDQUFDO0lBQ2pELElBQUksQ0FBQyxpQkFBaUI7UUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLDBEQUEwRCxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FBQyxTQUFpQix3QkFBd0IsRUFBRTtJQUN6RSxNQUFNLFlBQVksR0FBRyxlQUFlLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDN0MsTUFBTSxFQUFFLEdBQUcsZ0JBQWdCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDMUMsaUhBQWlIO0lBQ2pILDZDQUE2QztJQUM3QyxFQUFFLENBQUMsSUFBSSxDQUFDOzs7Ozs7Ozs7R0FTUCxDQUFDLENBQUM7SUFDSCw4RkFBOEY7SUFDOUYscUJBQXFCLENBQUMsRUFBRSxFQUFFLENBQUMsaUJBQWlCLENBQUMsQ0FBQyxDQUFDO0lBQy9DLHdHQUF3RztJQUN4RyxzQkFBc0IsQ0FBQyxFQUFFLEVBQUUsMkJBQTJCLEVBQUUsNEJBQTRCLEVBQUUsRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUVwRyxNQUFNLGdCQUFnQixHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMscUVBQXFFLENBQUMsQ0FBQztJQUMzRyxNQUFNLGVBQWUsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDOzs7R0FHbEMsQ0FBQyxDQUFDO0lBQ0gsTUFBTSxnQkFBZ0IsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLCtDQUErQyxDQUFDLENBQUM7SUFDckYsTUFBTSxnQkFBZ0IsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLG1EQUFtRCxDQUFDLENBQUM7SUFDekYsTUFBTSxtQkFBbUIsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUNwQyw0RUFBNEUsQ0FDN0UsQ0FBQztJQUNGLE1BQU0sa0JBQWtCLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FDbkMsaUVBQWlFLENBQ2xFLENBQUM7SUFDRixNQUFNLHdCQUF3QixHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQ3pDLHdGQUF3RixDQUN6RixDQUFDO0lBRUYsT0FBTztRQUNMLE1BQU0sRUFBRSxZQUFZO1FBQ3BCLFdBQVcsQ0FBQyxLQUFLO1lBQ2YsTUFBTSxJQUFJLEdBQUcsa0JBQWtCLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO1lBQzdDLE1BQU0sWUFBWSxHQUFHLDZCQUE2QixDQUFDLEtBQUssRUFBRSxZQUFZLENBQUMsQ0FBQztZQUN4RSxNQUFNLFdBQVcsR0FBRyxnQkFBZ0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDckQsTUFBTSxTQUFTLEdBQUcsSUFBSSxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQztZQUMzQyw2R0FBNkc7WUFDN0csdUdBQXVHO1lBQ3ZHLEVBQUUsQ0FBQyxJQUFJLENBQUMsaUJBQWlCLENBQUMsQ0FBQztZQUMzQixJQUFJLENBQUM7Z0JBQ0gsTUFBTSxFQUFFLE9BQU8sRUFBRSxHQUFHLGdCQUFnQixDQUFDLEdBQUcsRUFBb0MsQ0FBQztnQkFDN0UsTUFBTSxNQUFNLEdBQUcsZUFBZSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxXQUFXLEVBQUUsU0FBUyxDQUFDLENBQUM7Z0JBQ3hGLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxZQUFZLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUUsQ0FBQyxDQUFDLENBQUM7Z0JBQzlGLEVBQUUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7Z0JBQ2xCLE9BQU8sS0FBSyxDQUFDO1lBQ2YsQ0FBQztZQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7Z0JBQ2YsRUFBRSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQztnQkFDcEIsTUFBTSxLQUFLLENBQUM7WUFDZCxDQUFDO1FBQ0gsQ0FBQztRQUNELFVBQVUsQ0FBQyxNQUFNLEdBQUcsRUFBRTtZQUNwQixNQUFNLFlBQVksR0FBRyx1QkFBdUIsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7WUFDbEUsNEdBQTRHO1lBQzVHLHNCQUFzQjtZQUN0QixNQUFNLEtBQUssR0FBRyxzQkFBc0IsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7WUFFbkQsSUFBSSxJQUFzQyxDQUFDO1lBQzNDLElBQUksWUFBWSxLQUFLLFNBQVMsSUFBSSxLQUFLLEtBQUssU0FBUyxFQUFFLENBQUM7Z0JBQ3RELElBQUksR0FBRyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsWUFBWSxFQUFFLEtBQUssQ0FBQyxDQUFDO1lBQzNELENBQUM7aUJBQU0sSUFBSSxZQUFZLEtBQUssU0FBUyxFQUFFLENBQUM7Z0JBQ3RDLElBQUksR0FBRyxtQkFBbUIsQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUM7WUFDL0MsQ0FBQztpQkFBTSxJQUFJLEtBQUssS0FBSyxTQUFTLEVBQUUsQ0FBQztnQkFDL0IsSUFBSSxHQUFHLGtCQUFrQixDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUN2QyxDQUFDO2lCQUFNLENBQUM7Z0JBQ04sSUFBSSxHQUFHLGdCQUFnQixDQUFDLEdBQUcsRUFBRSxDQUFDO1lBQ2hDLENBQUM7WUFDRCxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzFELENBQUM7UUFDRCxxR0FBcUc7UUFDckcsMEdBQTBHO1FBQzFHLDBHQUEwRztRQUMxRywwQkFBMEI7UUFDMUIsV0FBVyxDQUFDLFlBQVk7WUFDdEIsTUFBTSxVQUFVLEdBQUcsNkJBQTZCLENBQUMsWUFBWSxDQUFDLENBQUM7WUFDL0QsSUFBSSxVQUFVLEtBQUssSUFBSTtnQkFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7WUFDbkUsT0FBTyxnQkFBZ0IsQ0FBQyxFQUFFLEVBQUUsdUJBQXVCLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFDbkUsQ0FBQztRQUNELEtBQUs7WUFDSCxFQUFFLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDYixDQUFDO0tBQ0YsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFTLHFCQUFxQjtJQUM1QixrQkFBa0IsS0FBSyxlQUFlLEVBQUUsQ0FBQztJQUN6QyxPQUFPLGtCQUFrQixDQUFDO0FBQzVCLENBQUM7QUFFRCxNQUFNLFVBQVUsV0FBVyxDQUFDLEtBQXVCO0lBQ2pELE9BQU8scUJBQXFCLEVBQUUsQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDcEQsQ0FBQztBQUVELE1BQU0sVUFBVSxVQUFVLENBQUMsTUFBeUI7SUFDbEQsT0FBTyxxQkFBcUIsRUFBRSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNwRCxDQUFDO0FBRUQsTUFBTSxVQUFVLHVCQUF1QjtJQUNyQyxJQUFJLENBQUMsa0JBQWtCO1FBQUUsT0FBTztJQUNoQyxrQkFBa0IsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUMzQixrQkFBa0IsR0FBRyxJQUFJLENBQUM7QUFDNUIsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/execute-local-write.d.ts b/packages/loopover-miner/lib/execute-local-write.d.ts deleted file mode 100644 index 092044c53e..0000000000 --- a/packages/loopover-miner/lib/execute-local-write.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { LocalWriteActionSpec } from "@loopover/engine"; -export type ExecuteLocalWriteResult = { - action: string; - stdout: string; - stderr: string; - code: number | null; - timedOut: boolean; -}; -export declare function executeLocalWrite(spec: LocalWriteActionSpec, options?: { - cwd?: string; - env?: NodeJS.ProcessEnv; - timeoutMs?: number; -}): Promise; diff --git a/packages/loopover-miner/lib/execute-local-write.js b/packages/loopover-miner/lib/execute-local-write.js deleted file mode 100644 index 3d9f66de68..0000000000 --- a/packages/loopover-miner/lib/execute-local-write.js +++ /dev/null @@ -1,42 +0,0 @@ -// Real executeLocalWrite implementation (#5132, Wave 3.5). Mirrors coding-agent-construction.js's -// createRealCliSubprocessSpawn pattern (real child_process, resolve-not-reject on error/timeout so a -// killed/errored process's partial output -- e.g. an auth failure line on stderr -- is never lost to an -// unhandled rejection) but for LocalWriteActionSpec.command: a single shell-safe string (built with -// packages/loopover-engine/src/miner/local-write-tools.ts's own single-quote escaping), not the -// cmd/args-array CliSubprocessSpawnFn contract the coding-agent driver itself uses. Runs it via `sh -c` in -// the given working directory. Per local-write-tools.ts's own boundary comment, this always runs with -// whatever `gh`/`git` credentials are already configured in that environment -- loopover never performs -// the write itself. -import { spawn } from "node:child_process"; -const DEFAULT_TIMEOUT_MS = 120_000; -export function executeLocalWrite(spec, options = {}) { - const cwd = options.cwd ?? process.cwd(); - const env = options.env ?? process.env; - const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS; - return new Promise((resolve) => { - const child = spawn("sh", ["-c", spec.command], { cwd, env, stdio: ["ignore", "pipe", "pipe"] }); - let stdout = ""; - let stderr = ""; - const timer = setTimeout(() => { - child.kill("SIGKILL"); - resolve({ action: spec.action, stdout, stderr, code: null, timedOut: true }); - }, timeoutMs); - child.stdout?.on("data", (chunk) => { - stdout += chunk.toString("utf8"); - }); - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString("utf8"); - }); - child.on("error", (err) => { - // A spawn-level error (e.g. no `sh` on PATH) fires before the child ever produces output -- mirrors - // createRealCliSubprocessSpawn's own identical handling. - clearTimeout(timer); - resolve({ action: spec.action, stdout, stderr: err.message, code: null, timedOut: false }); - }); - child.on("close", (code) => { - clearTimeout(timer); - resolve({ action: spec.action, stdout, stderr, code, timedOut: false }); - }); - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhlY3V0ZS1sb2NhbC13cml0ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImV4ZWN1dGUtbG9jYWwtd3JpdGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsa0dBQWtHO0FBQ2xHLHFHQUFxRztBQUNyRyx3R0FBd0c7QUFDeEcsb0dBQW9HO0FBQ3BHLGdHQUFnRztBQUNoRywyR0FBMkc7QUFDM0csc0dBQXNHO0FBQ3RHLHdHQUF3RztBQUN4RyxvQkFBb0I7QUFFcEIsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRzNDLE1BQU0sa0JBQWtCLEdBQUcsT0FBTyxDQUFDO0FBVW5DLE1BQU0sVUFBVSxpQkFBaUIsQ0FDL0IsSUFBMEIsRUFDMUIsVUFBeUUsRUFBRTtJQUUzRSxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUN6QyxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUM7SUFDdkMsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFFLE9BQU8sQ0FBQyxTQUFvQixDQUFDLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQztJQUUxRyxPQUFPLElBQUksT0FBTyxDQUEwQixDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ3RELE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsQ0FBQyxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNqRyxJQUFJLE1BQU0sR0FBRyxFQUFFLENBQUM7UUFDaEIsSUFBSSxNQUFNLEdBQUcsRUFBRSxDQUFDO1FBQ2hCLE1BQU0sS0FBSyxHQUFHLFVBQVUsQ0FBQyxHQUFHLEVBQUU7WUFDNUIsS0FBSyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQztZQUN0QixPQUFPLENBQUMsRUFBRSxNQUFNLEVBQUUsSUFBSSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDL0UsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBQ2QsS0FBSyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBYSxFQUFFLEVBQUU7WUFDekMsTUFBTSxJQUFJLEtBQUssQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDbkMsQ0FBQyxDQUFDLENBQUM7UUFDSCxLQUFLLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxLQUFhLEVBQUUsRUFBRTtZQUN6QyxNQUFNLElBQUksS0FBSyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUNuQyxDQUFDLENBQUMsQ0FBQztRQUNILEtBQUssQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsR0FBVSxFQUFFLEVBQUU7WUFDL0Isb0dBQW9HO1lBQ3BHLHlEQUF5RDtZQUN6RCxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDcEIsT0FBTyxDQUFDLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLENBQUMsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDN0YsQ0FBQyxDQUFDLENBQUM7UUFDSCxLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLElBQW1CLEVBQUUsRUFBRTtZQUN4QyxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDcEIsT0FBTyxDQUFDLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDMUUsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDLENBQUMsQ0FBQztBQUNMLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/feasibility-cli.d.ts b/packages/loopover-miner/lib/feasibility-cli.d.ts deleted file mode 100644 index ef1e18181d..0000000000 --- a/packages/loopover-miner/lib/feasibility-cli.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FeasibilityClaimStatus, FeasibilityDuplicateClusterRisk, FeasibilityGateInput, FeasibilityGateResult, FeasibilityIssueStatus } from "@loopover/engine"; -export type ParsedFeasibilityArgs = { - claimStatus: FeasibilityClaimStatus; - duplicateClusterRisk: FeasibilityDuplicateClusterRisk; - issueStatus: FeasibilityIssueStatus; - found: boolean; - json: boolean; -} | { - error: string; -}; -export declare function parseFeasibilityArgs(args: string[]): ParsedFeasibilityArgs; -export type RunFeasibilityCliOptions = { - buildFeasibilityVerdict?: (input: FeasibilityGateInput) => FeasibilityGateResult; -}; -export declare function runFeasibilityCli(args: string[], options?: RunFeasibilityCliOptions): number; diff --git a/packages/loopover-miner/lib/feasibility-cli.js b/packages/loopover-miner/lib/feasibility-cli.js deleted file mode 100644 index 98bc75d044..0000000000 --- a/packages/loopover-miner/lib/feasibility-cli.js +++ /dev/null @@ -1,85 +0,0 @@ -/** `feasibility` CLI command (#4270): a thin parse -> execute -> render wrapper around the engine's pure - * `buildFeasibilityVerdict` composer. Purely local — no network, no filesystem — so it never needs the - * npm-registry update check other subcommands opt into. */ -import { buildFeasibilityVerdict } from "@loopover/engine"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -const CLAIM_STATUSES = ["unclaimed", "claimed", "solved", "unknown"]; -const DUPLICATE_CLUSTER_RISKS = ["none", "low", "medium", "high"]; -const ISSUE_STATUSES = [ - "ready", - "needs_proof", - "hold", - "do_not_use", - "duplicate", - "invalid", - "missing", -]; -/** Plain `Array.includes` doesn't narrow a `string` argument down to the array's literal element type, so this - * small type-guard wrapper does it explicitly wherever a parsed CLI token needs to become one of these enums. */ -function isOneOf(value, allowed) { - return allowed.includes(value); -} -const FEASIBILITY_USAGE = "Usage: loopover-miner feasibility [--not-found] [--json]\n" + - ` claimStatus: ${CLAIM_STATUSES.join("|")}\n` + - ` duplicateClusterRisk: ${DUPLICATE_CLUSTER_RISKS.join("|")}\n` + - ` issueStatus: ${ISSUE_STATUSES.join("|")}`; -export function parseFeasibilityArgs(args) { - const options = { json: false, found: true }; - const positional = []; - for (const token of args) { - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--not-found") { - options.found = false; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 3) { - return { error: FEASIBILITY_USAGE }; - } - // positional.length === 3 was just verified above, so this tuple cast is safe. - const [claimStatus, duplicateClusterRisk, issueStatus] = positional; - if (!isOneOf(claimStatus, CLAIM_STATUSES)) { - return { error: `claimStatus must be one of: ${CLAIM_STATUSES.join(", ")}.` }; - } - if (!isOneOf(duplicateClusterRisk, DUPLICATE_CLUSTER_RISKS)) { - return { error: `duplicateClusterRisk must be one of: ${DUPLICATE_CLUSTER_RISKS.join(", ")}.` }; - } - if (!isOneOf(issueStatus, ISSUE_STATUSES)) { - return { error: `issueStatus must be one of: ${ISSUE_STATUSES.join(", ")}.` }; - } - return { - claimStatus, - duplicateClusterRisk, - issueStatus, - found: options.found, - json: options.json, - }; -} -export function runFeasibilityCli(args, options = {}) { - const parsed = parseFeasibilityArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const buildVerdict = options.buildFeasibilityVerdict ?? buildFeasibilityVerdict; - const verdict = buildVerdict({ - found: parsed.found, - claimStatus: parsed.claimStatus, - duplicateClusterRisk: parsed.duplicateClusterRisk, - issueStatus: parsed.issueStatus, - }); - if (parsed.json) { - console.log(JSON.stringify(verdict, null, 2)); - } - else { - console.log(`${verdict.verdict}: ${verdict.summary}`); - } - return 0; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmVhc2liaWxpdHktY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZmVhc2liaWxpdHktY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzsyREFFMkQ7QUFDM0QsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFRM0QsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWhFLE1BQU0sY0FBYyxHQUFHLENBQUMsV0FBVyxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsU0FBUyxDQUFzRCxDQUFDO0FBQzFILE1BQU0sdUJBQXVCLEdBQUcsQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxNQUFNLENBQStELENBQUM7QUFDaEksTUFBTSxjQUFjLEdBQUc7SUFDckIsT0FBTztJQUNQLGFBQWE7SUFDYixNQUFNO0lBQ04sWUFBWTtJQUNaLFdBQVc7SUFDWCxTQUFTO0lBQ1QsU0FBUztDQUMyQyxDQUFDO0FBRXZEO2tIQUNrSDtBQUNsSCxTQUFTLE9BQU8sQ0FBbUIsS0FBYSxFQUFFLE9BQXFCO0lBQ3JFLE9BQVEsT0FBNkIsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDeEQsQ0FBQztBQUVELE1BQU0saUJBQWlCLEdBQ3JCLCtHQUErRztJQUMvRyxrQkFBa0IsY0FBYyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSTtJQUM5QywyQkFBMkIsdUJBQXVCLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJO0lBQ2hFLGtCQUFrQixjQUFjLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7QUFZL0MsTUFBTSxVQUFVLG9CQUFvQixDQUFDLElBQWM7SUFDakQsTUFBTSxPQUFPLEdBQUcsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQztJQUM3QyxNQUFNLFVBQVUsR0FBYSxFQUFFLENBQUM7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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/forge-config.d.ts b/packages/loopover-miner/lib/forge-config.d.ts deleted file mode 100644 index 5930f4be63..0000000000 --- a/packages/loopover-miner/lib/forge-config.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** Per-tenant forge configuration (#4784): the GitHub-specific protocol details that discovery used to hardcode, - * gathered behind one resolver so a non-github.com tenant (GitHub Enterprise, or another GitHub-compatible forge) - * can override them. loopover's own github.com conventions survive only as `DEFAULT_FORGE_CONFIG` — calling - * `resolveForgeConfig()` with no overrides is byte-identical to the pre-#4784 hardcoded fan-out behavior, which is - * what keeps the existing loopover discovery path unchanged. Executes the #4780 repo-agnostic-capability-audit - * checklist (forge abstraction, configurable credential env var, configurable user-agent). */ -/** Per-tenant forge configuration (#4784). Every field is a string knob defaulting to the github.com value in - * `DEFAULT_FORGE_CONFIG`; a tenant overrides only what differs for their forge. */ -export type ForgeConfig = { - apiBaseUrl: string; - apiVersion: string; - apiVersionHeader: string; - acceptHeader: string; - userAgent: string; - repoPathPrefix: string; - searchEndpoint: string; - searchQualifiers: string; - tokenEnvVar: string; -}; -/** The github.com defaults every forge field falls back to. Frozen so a caller can't mutate the shared baseline. */ -export declare const DEFAULT_FORGE_CONFIG: Readonly; -/** - * Resolve a full forge config from partial per-tenant overrides. Every field is an independent string knob that - * falls back to its github.com default when the override is missing, non-string, or blank — so a partial override - * (say, only `apiBaseUrl` for a GitHub Enterprise host) still yields a complete, usable config. - */ -export declare function resolveForgeConfig(overrides?: Partial): ForgeConfig; diff --git a/packages/loopover-miner/lib/forge-config.js b/packages/loopover-miner/lib/forge-config.js deleted file mode 100644 index beb26fe8d4..0000000000 --- a/packages/loopover-miner/lib/forge-config.js +++ /dev/null @@ -1,35 +0,0 @@ -/** Per-tenant forge configuration (#4784): the GitHub-specific protocol details that discovery used to hardcode, - * gathered behind one resolver so a non-github.com tenant (GitHub Enterprise, or another GitHub-compatible forge) - * can override them. loopover's own github.com conventions survive only as `DEFAULT_FORGE_CONFIG` — calling - * `resolveForgeConfig()` with no overrides is byte-identical to the pre-#4784 hardcoded fan-out behavior, which is - * what keeps the existing loopover discovery path unchanged. Executes the #4780 repo-agnostic-capability-audit - * checklist (forge abstraction, configurable credential env var, configurable user-agent). */ -/** The github.com defaults every forge field falls back to. Frozen so a caller can't mutate the shared baseline. */ -export const DEFAULT_FORGE_CONFIG = Object.freeze({ - apiBaseUrl: "https://api.github.com", - apiVersion: "2022-11-28", - apiVersionHeader: "x-github-api-version", - acceptHeader: "application/vnd.github+json", - userAgent: "loopover-miner", - repoPathPrefix: "/repos", - searchEndpoint: "/search/issues", - searchQualifiers: "state:open type:issue", - tokenEnvVar: "GITHUB_TOKEN", -}); -function trimmedStringOr(value, fallback) { - return typeof value === "string" && value.trim() ? value.trim() : fallback; -} -/** - * Resolve a full forge config from partial per-tenant overrides. Every field is an independent string knob that - * falls back to its github.com default when the override is missing, non-string, or blank — so a partial override - * (say, only `apiBaseUrl` for a GitHub Enterprise host) still yields a complete, usable config. - */ -export function resolveForgeConfig(overrides = {}) { - const source = overrides && typeof overrides === "object" ? overrides : {}; - const resolved = {}; - for (const key of Object.keys(DEFAULT_FORGE_CONFIG)) { - resolved[key] = trimmedStringOr(source[key], DEFAULT_FORGE_CONFIG[key]); - } - return resolved; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9yZ2UtY29uZmlnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZm9yZ2UtY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs4RkFLOEY7QUFnQjlGLG9IQUFvSDtBQUNwSCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBMEIsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN2RSxVQUFVLEVBQUUsd0JBQXdCO0lBQ3BDLFVBQVUsRUFBRSxZQUFZO0lBQ3hCLGdCQUFnQixFQUFFLHNCQUFzQjtJQUN4QyxZQUFZLEVBQUUsNkJBQTZCO0lBQzNDLFNBQVMsRUFBRSxnQkFBZ0I7SUFDM0IsY0FBYyxFQUFFLFFBQVE7SUFDeEIsY0FBYyxFQUFFLGdCQUFnQjtJQUNoQyxnQkFBZ0IsRUFBRSx1QkFBdUI7SUFDekMsV0FBVyxFQUFFLGNBQWM7Q0FDNUIsQ0FBQyxDQUFDO0FBRUgsU0FBUyxlQUFlLENBQUMsS0FBYyxFQUFFLFFBQWdCO0lBQ3ZELE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUM7QUFDN0UsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsa0JBQWtCLENBQUMsWUFBa0MsRUFBRTtJQUNyRSxNQUFNLE1BQU0sR0FBRyxTQUFTLElBQUksT0FBTyxTQUFTLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUMzRSxNQUFNLFFBQVEsR0FBRyxFQUFpQixDQUFDO0lBQ25DLEtBQUssTUFBTSxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsQ0FBNkIsRUFBRSxDQUFDO1FBQ2hGLFFBQVEsQ0FBQyxHQUFHLENBQUMsR0FBRyxlQUFlLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDMUUsQ0FBQztJQUNELE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/github-token-resolution.d.ts b/packages/loopover-miner/lib/github-token-resolution.d.ts deleted file mode 100644 index d9120a73b1..0000000000 --- a/packages/loopover-miner/lib/github-token-resolution.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -export type GitHubTokenResolutionFetch = (url: string, init?: { - method?: string; - headers?: Record; - signal?: AbortSignal; -}) => Promise; -/** - * Same loopover-mcp session + API URL posture `resolveGitHubToken` uses for backend calls (#6487). - * Returns null when there is no session token on disk (fully-standalone AMS / no `loopover-mcp login`). - */ -export declare function resolveLoopoverBackendSession(env?: NodeJS.ProcessEnv): { - apiUrl: string; - sessionToken: string; -} | null; -/** - * Resolve a GitHub token for AMS's git operations (#6116). Returns null when nothing is available: no - * GITHUB_TOKEN override, no loopover-mcp session on disk, or the session-token fetch fails for any reason -- - * callers already treat a missing token as "git operations requiring auth will fail," the same failure mode - * as before this feature existed. - */ -export declare function resolveGitHubToken(env?: NodeJS.ProcessEnv, options?: { - fetchImpl?: GitHubTokenResolutionFetch; -}): Promise; -/** Test-only: clear the process-lifetime cache so one test's resolution can't leak into the next. */ -export declare function resetGitHubTokenResolutionForTesting(): void; -/** - * Offline-only check: does resolveGitHubToken have ANYTHING to try (a GITHUB_TOKEN override, or a - * loopover-mcp session recorded on disk), without making the network call resolveGitHubToken itself would - * make to actually verify it still works. For `doctor`/`status`-style diagnostics (status.js's - * checkGitHubTokenPresent), which are deliberately offline-only -- a genuinely expired or revoked session - * still reports "present" here; only an actual attempt (or resolveGitHubToken itself) discovers that. - */ -export declare function hasGitHubTokenSource(env?: NodeJS.ProcessEnv): boolean; diff --git a/packages/loopover-miner/lib/github-token-resolution.js b/packages/loopover-miner/lib/github-token-resolution.js deleted file mode 100644 index a364a93400..0000000000 --- a/packages/loopover-miner/lib/github-token-resolution.js +++ /dev/null @@ -1,140 +0,0 @@ -// GitHub-token resolution for AMS's git operations (#6116). Precedence: an explicit GITHUB_TOKEN env -// override always wins (a self-host operator's existing PAT setup keeps working, unchanged) -- otherwise, -// fetch a live token from the authenticated loopover-mcp session (POST /v1/auth/github/token, #6114/#6115), -// so `loopover-mcp login` alone becomes sufficient to run AMS against a repo the user has access to. -// -// Deliberately reimplements loopover-mcp's own config-file read here rather than depending on @loopover/mcp -// as a package: @loopover/miner and @loopover/mcp are separately-installable CLIs (the whole point of this -// milestone is that installing the GitHub App doesn't require BOTH), and a hard runtime dependency between -// them would mean installing one always pulls in the other just to read a config file format neither -// package publishes as a stable API. This mirrors loopover-mcp/bin/loopover-mcp.js's own configPath/ -// selectProfileName/apiUrl resolution logic (kept in sync by hand -- there is no shared module to import). -import { existsSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; -const DEFAULT_API_URL = "https://api.loopover.ai"; -const LEGACY_DEFAULT_API_URLS = new Set([ - "https://gittensory-api.zeronode.workers.dev", - "https://gittensory-api.aethereal.dev", -]); -const DEFAULT_PROFILE_NAME = "default"; -const GITHUB_TOKEN_FETCH_TIMEOUT_MS = 10_000; -function loopoverConfigPath(env) { - if (env.LOOPOVER_CONFIG_PATH) - return env.LOOPOVER_CONFIG_PATH; - if (env.LOOPOVER_CONFIG_DIR) - return join(env.LOOPOVER_CONFIG_DIR, "config.json"); - return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "loopover", "config.json"); -} -function loadLoopoverConfig(env) { - const configPath = loopoverConfigPath(env); - if (!existsSync(configPath)) - return {}; - try { - const parsed = JSON.parse(readFileSync(configPath, "utf8")); - return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; - } - catch { - return {}; - } -} -// Only ever called with an already-truthy candidate name (see selectProfileName below) -- no nullish -// fallback needed here, since a nullish/empty `value` never reaches this function in the first place. -function normalizeProfileName(value) { - const name = String(value).trim().toLowerCase(); - return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(name) ? name : DEFAULT_PROFILE_NAME; -} -// Mirrors loopover-mcp's own selectProfileName: an explicit request wins, else the config's own -// activeProfile (only if it names a real profile entry), else "default". -function selectProfileName(config, requestedName) { - if (requestedName) - return normalizeProfileName(requestedName); - const configured = config.activeProfile ? normalizeProfileName(config.activeProfile) : DEFAULT_PROFILE_NAME; - return config.profiles?.[configured] ? configured : DEFAULT_PROFILE_NAME; -} -function activeLoopoverProfile(env) { - const config = loadLoopoverConfig(env); - const profileName = selectProfileName(config, env.LOOPOVER_PROFILE); - return config.profiles?.[profileName] ?? {}; -} -function loopoverSessionToken(env) { - const token = activeLoopoverProfile(env).session?.token; - return typeof token === "string" && token ? token : null; -} -function loopoverApiUrl(env) { - if (env.LOOPOVER_API_URL) - return env.LOOPOVER_API_URL.replace(/\/+$/, ""); - const profileApiUrl = activeLoopoverProfile(env).apiUrl; - if (typeof profileApiUrl === "string" && profileApiUrl.trim()) { - const normalized = profileApiUrl.replace(/\/+$/, ""); - if (!LEGACY_DEFAULT_API_URLS.has(normalized)) - return normalized; - } - return DEFAULT_API_URL; -} -/** - * Same loopover-mcp session + API URL posture `resolveGitHubToken` uses for backend calls (#6487). - * Returns null when there is no session token on disk (fully-standalone AMS / no `loopover-mcp login`). - */ -export function resolveLoopoverBackendSession(env = process.env) { - const sessionToken = loopoverSessionToken(env); - if (!sessionToken) - return null; - return { apiUrl: loopoverApiUrl(env), sessionToken }; -} -async function fetchLiveGitHubTokenFromSession(sessionToken, apiUrl, fetchImpl) { - try { - const response = await fetchImpl(`${apiUrl}/v1/auth/github/token`, { - method: "POST", - headers: { authorization: `Bearer ${sessionToken}`, accept: "application/json" }, - signal: AbortSignal.timeout(GITHUB_TOKEN_FETCH_TIMEOUT_MS), - }); - if (!response.ok) - return null; - const payload = (await response.json().catch(() => null)); - return typeof payload?.token === "string" && payload.token ? payload.token : null; - } - catch { - return null; - } -} -// Process-lifetime cache of a SUCCESSFUL resolution only. A failure (no session, expired session, transient -// network error) is deliberately NOT cached -- it's retried on the next call instead, so a long-running AMS -// process can self-heal from a transient blip rather than being stuck treating the token as permanently -// unavailable for its entire remaining lifetime. -let cachedToken; -/** - * Resolve a GitHub token for AMS's git operations (#6116). Returns null when nothing is available: no - * GITHUB_TOKEN override, no loopover-mcp session on disk, or the session-token fetch fails for any reason -- - * callers already treat a missing token as "git operations requiring auth will fail," the same failure mode - * as before this feature existed. - */ -export async function resolveGitHubToken(env = process.env, options = {}) { - if (env.GITHUB_TOKEN) - return env.GITHUB_TOKEN; - if (cachedToken) - return cachedToken; - const sessionToken = loopoverSessionToken(env); - if (!sessionToken) - return null; - const fetchImpl = options.fetchImpl ?? fetch; - const fetched = await fetchLiveGitHubTokenFromSession(sessionToken, loopoverApiUrl(env), fetchImpl); - if (fetched) - cachedToken = fetched; - return fetched; -} -/** Test-only: clear the process-lifetime cache so one test's resolution can't leak into the next. */ -export function resetGitHubTokenResolutionForTesting() { - cachedToken = undefined; -} -/** - * Offline-only check: does resolveGitHubToken have ANYTHING to try (a GITHUB_TOKEN override, or a - * loopover-mcp session recorded on disk), without making the network call resolveGitHubToken itself would - * make to actually verify it still works. For `doctor`/`status`-style diagnostics (status.js's - * checkGitHubTokenPresent), which are deliberately offline-only -- a genuinely expired or revoked session - * still reports "present" here; only an actual attempt (or resolveGitHubToken itself) discovers that. - */ -export function hasGitHubTokenSource(env = process.env) { - return Boolean(env.GITHUB_TOKEN) || Boolean(loopoverSessionToken(env)); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2l0aHViLXRva2VuLXJlc29sdXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnaXRodWItdG9rZW4tcmVzb2x1dGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxxR0FBcUc7QUFDckcsMEdBQTBHO0FBQzFHLDRHQUE0RztBQUM1RyxxR0FBcUc7QUFDckcsRUFBRTtBQUNGLDRHQUE0RztBQUM1RywyR0FBMkc7QUFDM0csMkdBQTJHO0FBQzNHLHFHQUFxRztBQUNyRyxxR0FBcUc7QUFDckcsMkdBQTJHO0FBQzNHLE9BQU8sRUFBRSxVQUFVLEVBQUUsWUFBWSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ25ELE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDbEMsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQXFCakMsTUFBTSxlQUFlLEdBQUcseUJBQXlCLENBQUM7QUFDbEQsTUFBTSx1QkFBdUIsR0FBRyxJQUFJLEdBQUcsQ0FBQztJQUN0Qyw2Q0FBNkM7SUFDN0Msc0NBQXNDO0NBQ3ZDLENBQUMsQ0FBQztBQUNILE1BQU0sb0JBQW9CLEdBQUcsU0FBUyxDQUFDO0FBQ3ZDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDO0FBRTdDLFNBQVMsa0JBQWtCLENBQUMsR0FBc0I7SUFDaEQsSUFBSSxHQUFHLENBQUMsb0JBQW9CO1FBQUUsT0FBTyxHQUFHLENBQUMsb0JBQW9CLENBQUM7SUFDOUQsSUFBSSxHQUFHLENBQUMsbUJBQW1CO1FBQUUsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLG1CQUFtQixFQUFFLGFBQWEsQ0FBQyxDQUFDO0lBQ2pGLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxlQUFlLElBQUksSUFBSSxDQUFDLE9BQU8sRUFBRSxFQUFFLFNBQVMsQ0FBQyxFQUFFLFVBQVUsRUFBRSxhQUFhLENBQUMsQ0FBQztBQUM1RixDQUFDO0FBRUQsU0FBUyxrQkFBa0IsQ0FBQyxHQUFzQjtJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-chokepoint-persisted.d.ts b/packages/loopover-miner/lib/governor-chokepoint-persisted.d.ts deleted file mode 100644 index d63f3f5581..0000000000 --- a/packages/loopover-miner/lib/governor-chokepoint-persisted.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GovernorChokepointInput } from "@loopover/engine"; -import type { EvaluateGovernorChokepointGateResult } from "./governor-chokepoint.js"; -import type { GovernorState } from "./governor-state.js"; -import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js"; -export type GovernorChokepointInputPersisted = Omit & Partial>; -export declare function evaluateGovernorChokepointGatePersisted(input: GovernorChokepointInputPersisted, options?: { - governorState?: GovernorState; - append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry; -}): EvaluateGovernorChokepointGateResult; diff --git a/packages/loopover-miner/lib/governor-chokepoint-persisted.js b/packages/loopover-miner/lib/governor-chokepoint-persisted.js deleted file mode 100644 index e2fe86a39e..0000000000 --- a/packages/loopover-miner/lib/governor-chokepoint-persisted.js +++ /dev/null @@ -1,25 +0,0 @@ -import { evaluateGovernorChokepointGate } from "./governor-chokepoint.js"; -import { openGovernorState } from "./governor-state.js"; -export function evaluateGovernorChokepointGatePersisted(input, options = {}) { - const ownsGovernorState = options.governorState === undefined; - const governorState = options.governorState ?? openGovernorState(); - try { - const persistedRateLimit = governorState.loadRateLimitState(); - const persistedCapUsage = governorState.loadCapUsage(); - const resolvedInput = { - ...input, - rateLimitBuckets: input.rateLimitBuckets ?? persistedRateLimit.buckets, - rateLimitBackoffAttempts: input.rateLimitBackoffAttempts ?? persistedRateLimit.backoffAttempts, - capUsage: input.capUsage ?? persistedCapUsage, - }; - const gateOptions = options.append === undefined ? {} : { append: options.append }; - const result = evaluateGovernorChokepointGate(resolvedInput, gateOptions); - governorState.saveRateLimitState({ buckets: result.rateLimitBuckets, backoffAttempts: result.rateLimitBackoffAttempts }); - return result; - } - finally { - if (ownsGovernorState) - governorState.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItY2hva2Vwb2ludC1wZXJzaXN0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnb3Zlcm5vci1jaG9rZXBvaW50LXBlcnNpc3RlZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsOEJBQThCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUUxRSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQTRCeEQsTUFBTSxVQUFVLHVDQUF1QyxDQUNyRCxLQUF1QyxFQUN2QyxVQUdJLEVBQUU7SUFFTixNQUFNLGlCQUFpQixHQUFHLE9BQU8sQ0FBQyxhQUFhLEtBQUssU0FBUyxDQUFDO0lBQzlELE1BQU0sYUFBYSxHQUFHLE9BQU8sQ0FBQyxhQUFhLElBQUksaUJBQWlCLEVBQUUsQ0FBQztJQUNuRSxJQUFJLENBQUM7UUFDSCxNQUFNLGtCQUFrQixHQUFHLGFBQWEsQ0FBQyxrQkFBa0IsRUFBRSxDQUFDO1FBQzlELE1BQU0saUJBQWlCLEdBQUcsYUFBYSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQ3ZELE1BQU0sYUFBYSxHQUE0QjtZQUM3QyxHQUFHLEtBQUs7WUFDUixnQkFBZ0IsRUFBRSxLQUFLLENBQUMsZ0JBQWdCLElBQUksa0JBQWtCLENBQUMsT0FBTztZQUN0RSx3QkFBd0IsRUFBRSxLQUFLLENBQUMsd0JBQXdCLElBQUksa0JBQWtCLENBQUMsZUFBZTtZQUM5RixRQUFRLEVBQUUsS0FBSyxDQUFDLFFBQVEsSUFBSSxpQkFBaUI7U0FDOUMsQ0FBQztRQUNGLE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNuRixNQUFNLE1BQU0sR0FBRyw4QkFBOEIsQ0FBQyxhQUFhLEVBQUUsV0FBVyxDQUFDLENBQUM7UUFDMUUsYUFBYSxDQUFDLGtCQUFrQixDQUFDLEVBQUUsT0FBTyxFQUFFLE1BQU0sQ0FBQyxnQkFBZ0IsRUFBRSxlQUFlLEVBQUUsTUFBTSxDQUFDLHdCQUF3QixFQUFFLENBQUMsQ0FBQztRQUN6SCxPQUFPLE1BQU0sQ0FBQztJQUNoQixDQUFDO1lBQVMsQ0FBQztRQUNULElBQUksaUJBQWlCO1lBQUUsYUFBYSxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQy9DLENBQUM7QUFDSCxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-chokepoint.d.ts b/packages/loopover-miner/lib/governor-chokepoint.d.ts deleted file mode 100644 index c122591795..0000000000 --- a/packages/loopover-miner/lib/governor-chokepoint.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { GovernorChokepointInput, GovernorDecision, WriteRateLimitBackoffStore, WriteRateLimitBucketStore } from "@loopover/engine"; -import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js"; -export type EvaluateGovernorChokepointGateResult = { - decision: GovernorDecision; - recorded: GovernorLedgerEntry; - rateLimitBuckets: WriteRateLimitBucketStore; - rateLimitBackoffAttempts: WriteRateLimitBackoffStore; -}; -/** - * Evaluate a write action against the full Governor precedence ladder, persist the resulting ledger event, and - * advance rate-limit bucket/backoff state only for the two outcomes that actually consumed (or were denied at) - * the rate-limit stage: a final `"allow"` verdict advances the bucket, and a `"rate_limit"`-stage denial bumps - * backoff. Every other stage -- kill-switch, dry-run, budget-cap, non-convergence, reputation-throttle, - * self-plagiarism, internal_error -- denies for a reason unrelated to rate limiting and must leave bucket/backoff - * state untouched, since no real write happened and the rate-limit stage's own "allowed" sub-verdict (still - * present in `decision.detail.rateLimit` once that stage has cleared) does not mean the action was ultimately - * allowed. - */ -export declare function evaluateGovernorChokepointGate(input: GovernorChokepointInput, options?: { - append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry; -}): EvaluateGovernorChokepointGateResult; diff --git a/packages/loopover-miner/lib/governor-chokepoint.js b/packages/loopover-miner/lib/governor-chokepoint.js deleted file mode 100644 index de3bfa3968..0000000000 --- a/packages/loopover-miner/lib/governor-chokepoint.js +++ /dev/null @@ -1,33 +0,0 @@ -// The Governor chokepoint gate (#2340). Wraps the pure `evaluateGovernorChokepoint` engine decision with the -// two stateful side effects every caller needs: persisting the resulting ledger event, and (only when the -// rate-limit stage actually ran) advancing/backing-off the rate-limit bucket state. This is the ONLY sanctioned -// call site a real write action (open_pr, file_issue, apply_labels, post_eligibility_comment, create_branch, -// delete_branch, generate_tests) should be gated through. -import { clearWriteRateLimitBackoff, evaluateGovernorChokepoint, recordWriteRateLimitAllowed, recordWriteRateLimitDenied, } from "@loopover/engine"; -import { appendGovernorEvent } from "./governor-ledger.js"; -/** - * Evaluate a write action against the full Governor precedence ladder, persist the resulting ledger event, and - * advance rate-limit bucket/backoff state only for the two outcomes that actually consumed (or were denied at) - * the rate-limit stage: a final `"allow"` verdict advances the bucket, and a `"rate_limit"`-stage denial bumps - * backoff. Every other stage -- kill-switch, dry-run, budget-cap, non-convergence, reputation-throttle, - * self-plagiarism, internal_error -- denies for a reason unrelated to rate limiting and must leave bucket/backoff - * state untouched, since no real write happened and the rate-limit stage's own "allowed" sub-verdict (still - * present in `decision.detail.rateLimit` once that stage has cleared) does not mean the action was ultimately - * allowed. - */ -export function evaluateGovernorChokepointGate(input, options = {}) { - const append = options.append ?? appendGovernorEvent; - const decision = evaluateGovernorChokepoint(input); - const recorded = append(decision.ledgerEvent); - let rateLimitBuckets = input.rateLimitBuckets; - let rateLimitBackoffAttempts = input.rateLimitBackoffAttempts; - if (decision.stage === "allow") { - rateLimitBuckets = recordWriteRateLimitAllowed(input.rateLimitBuckets, input.actionClass, input.repoFullName, input.nowMs, input.rateLimitPolicies); - rateLimitBackoffAttempts = clearWriteRateLimitBackoff(input.rateLimitBackoffAttempts, input.actionClass, input.repoFullName); - } - else if (decision.stage === "rate_limit") { - rateLimitBackoffAttempts = recordWriteRateLimitDenied(input.rateLimitBackoffAttempts, input.actionClass, input.repoFullName); - } - return { decision, recorded, rateLimitBuckets, rateLimitBackoffAttempts }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItY2hva2Vwb2ludC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImdvdmVybm9yLWNob2tlcG9pbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsNkdBQTZHO0FBQzdHLDBHQUEwRztBQUMxRyxnSEFBZ0g7QUFDaEgsNkdBQTZHO0FBQzdHLDBEQUEwRDtBQUUxRCxPQUFPLEVBQ0wsMEJBQTBCLEVBQzFCLDBCQUEwQixFQUMxQiwyQkFBMkIsRUFDM0IsMEJBQTBCLEdBQzNCLE1BQU0sa0JBQWtCLENBQUM7QUFFMUIsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFVM0Q7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxVQUFVLDhCQUE4QixDQUM1QyxLQUE4QixFQUM5QixVQUFpRixFQUFFO0lBRW5GLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxNQUFNLElBQUksbUJBQW1CLENBQUM7SUFDckQsTUFBTSxRQUFRLEdBQUcsMEJBQTBCLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDbkQsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxXQUF1QyxDQUFDLENBQUM7SUFFMUUsSUFBSSxnQkFBZ0IsR0FBRyxLQUFLLENBQUMsZ0JBQWdCLENBQUM7SUFDOUMsSUFBSSx3QkFBd0IsR0FBRyxLQUFLLENBQUMsd0JBQXdCLENBQUM7SUFDOUQsSUFBSSxRQUFRLENBQUMsS0FBSyxLQUFLLE9BQU8sRUFBRSxDQUFDO1FBQy9CLGdCQUFnQixHQUFHLDJCQUEyQixDQUM1QyxLQUFLLENBQUMsZ0JBQWdCLEVBQ3RCLEtBQUssQ0FBQyxXQUFXLEVBQ2pCLEtBQUssQ0FBQyxZQUFZLEVBQ2xCLEtBQUssQ0FBQyxLQUFLLEVBQ1gsS0FBSyxDQUFDLGlCQUFpQixDQUN4QixDQUFDO1FBQ0Ysd0JBQXdCLEdBQUcsMEJBQTBCLENBQUMsS0FBSyxDQUFDLHdCQUF3QixFQUFFLEtBQUssQ0FBQyxXQUFXLEVBQUUsS0FBSyxDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQy9ILENBQUM7U0FBTSxJQUFJLFFBQVEsQ0FBQyxLQUFLLEtBQUssWUFBWSxFQUFFLENBQUM7UUFDM0Msd0JBQXdCLEdBQUcsMEJBQTBCLENBQUMsS0FBSyxDQUFDLHdCQUF3QixFQUFFLEtBQUssQ0FBQyxXQUFXLEVBQUUsS0FBSyxDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQy9ILENBQUM7SUFFRCxPQUFPLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxnQkFBZ0IsRUFBRSx3QkFBd0IsRUFBRSxDQUFDO0FBQzVFLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-kill-switch.d.ts b/packages/loopover-miner/lib/governor-kill-switch.d.ts deleted file mode 100644 index 191fa6e423..0000000000 --- a/packages/loopover-miner/lib/governor-kill-switch.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { MinerKillSwitchScope } from "@loopover/engine"; -import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js"; -export type CheckMinerKillSwitchInput = { - repoPaused?: boolean; - env?: Record; -}; -export type CheckMinerKillSwitchResult = { - scope: MinerKillSwitchScope; - active: boolean; -}; -/** - * Resolve the current kill-switch scope for a repo from process env plus a per-repo paused flag (typically - * `MinerGoalSpec.killSwitch.paused` from the repo's parsed `.loopover-miner.yml`). - */ -export declare function checkMinerKillSwitch(input?: CheckMinerKillSwitchInput): CheckMinerKillSwitchResult; -export type RecordMinerKillSwitchTransitionInput = { - repoFullName?: string; - actionClass: string; - previousScope: MinerKillSwitchScope; - scope: MinerKillSwitchScope; -}; -/** - * Record a kill-switch state transition to the governor ledger. No-op (returns null, appends nothing) when the - * scope has not actually changed since the previous check — callers own tracking the previous scope (in-memory - * or persisted); this module holds no state of its own. - */ -export declare function recordMinerKillSwitchTransition(input: RecordMinerKillSwitchTransitionInput, options?: { - append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry; -}): GovernorLedgerEntry | null; diff --git a/packages/loopover-miner/lib/governor-kill-switch.js b/packages/loopover-miner/lib/governor-kill-switch.js deleted file mode 100644 index 3c479144f3..0000000000 --- a/packages/loopover-miner/lib/governor-kill-switch.js +++ /dev/null @@ -1,29 +0,0 @@ -// Governor kill-switch gate (#2341). Resolves whether miner write activity is currently halted (globally, via -// env, or for one repo, via its .loopover-miner.yml MinerGoalSpec) and records STATE TRANSITIONS to the -// append-only governor ledger. Every-check allow/deny recording for a real write action is the fail-closed -// Governor chokepoint's job (#2340), which consults this module first in its "safest wins" precedence. -import { buildMinerKillSwitchTransitionGovernorLedgerEvent, isGlobalMinerKillSwitch, isMinerKillSwitchActive, resolveMinerKillSwitch, } from "@loopover/engine"; -import { appendGovernorEvent } from "./governor-ledger.js"; -/** - * Resolve the current kill-switch scope for a repo from process env plus a per-repo paused flag (typically - * `MinerGoalSpec.killSwitch.paused` from the repo's parsed `.loopover-miner.yml`). - */ -export function checkMinerKillSwitch(input = {}) { - const env = input.env ?? process.env; - const global = isGlobalMinerKillSwitch(env); - const scope = resolveMinerKillSwitch({ global, repoPaused: input.repoPaused }); - return { scope, active: isMinerKillSwitchActive(scope) }; -} -/** - * Record a kill-switch state transition to the governor ledger. No-op (returns null, appends nothing) when the - * scope has not actually changed since the previous check — callers own tracking the previous scope (in-memory - * or persisted); this module holds no state of its own. - */ -export function recordMinerKillSwitchTransition(input, options = {}) { - const event = buildMinerKillSwitchTransitionGovernorLedgerEvent(input); - if (!event) - return null; - const append = options.append ?? appendGovernorEvent; - return append(event); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3Ita2lsbC1zd2l0Y2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnb3Zlcm5vci1raWxsLXN3aXRjaC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw4R0FBOEc7QUFDOUcsd0dBQXdHO0FBQ3hHLDJHQUEyRztBQUMzRyx1R0FBdUc7QUFFdkcsT0FBTyxFQUNMLGlEQUFpRCxFQUNqRCx1QkFBdUIsRUFDdkIsdUJBQXVCLEVBQ3ZCLHNCQUFzQixHQUN2QixNQUFNLGtCQUFrQixDQUFDO0FBRTFCLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBYTNEOzs7R0FHRztBQUNILE1BQU0sVUFBVSxvQkFBb0IsQ0FBQyxRQUFtQyxFQUFFO0lBQ3hFLE1BQU0sR0FBRyxHQUFHLEtBQUssQ0FBQyxHQUFHLElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQztJQUNyQyxNQUFNLE1BQU0sR0FBRyx1QkFBdUIsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUM1QyxNQUFNLEtBQUssR0FBRyxzQkFBc0IsQ0FBQyxFQUFFLE1BQU0sRUFBRSxVQUFVLEVBQUUsS0FBSyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUM7SUFDL0UsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsdUJBQXVCLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztBQUMzRCxDQUFDO0FBU0Q7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSwrQkFBK0IsQ0FDN0MsS0FBMkMsRUFDM0MsVUFBaUYsRUFBRTtJQUVuRixNQUFNLEtBQUssR0FBRyxpREFBaUQsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN2RSxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3hCLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxNQUFNLElBQUksbUJBQW1CLENBQUM7SUFDckQsT0FBTyxNQUFNLENBQUMsS0FBaUMsQ0FBQyxDQUFDO0FBQ25ELENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-ledger-cli.d.ts b/packages/loopover-miner/lib/governor-ledger-cli.d.ts deleted file mode 100644 index dd16c13329..0000000000 --- a/packages/loopover-miner/lib/governor-ledger-cli.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { GovernorPauseCliOptions } from "./governor-pause-cli.js"; -import type { GovernorLedger, GovernorLedgerEntry } from "./governor-ledger.js"; -export type GovernorLedgerEventType = "allowed" | "denied" | "throttled" | "kill_switch"; -export type ParsedGovernorListArgs = { - json: boolean; - repoFullName: string | null; - type: GovernorLedgerEventType | null; -} | { - error: string; -}; -export type GovernorCliOptions = { - initGovernorLedger?: () => GovernorLedger; - nowMs?: number; -} & GovernorPauseCliOptions; -export declare function parseGovernorListArgs(args: string[]): ParsedGovernorListArgs; -export declare function filterGovernorEvents(events: GovernorLedgerEntry[], options?: { - type?: string | null; -}): GovernorLedgerEntry[]; -export declare function renderGovernorTable(events: GovernorLedgerEntry[]): string; -export declare function runGovernorList(args: string[], options?: GovernorCliOptions): Promise; -export declare function runGovernorCli(subcommand: string | undefined, args: string[], options?: GovernorCliOptions): Promise; diff --git a/packages/loopover-miner/lib/governor-ledger-cli.js b/packages/loopover-miner/lib/governor-ledger-cli.js deleted file mode 100644 index c7a5179a36..0000000000 --- a/packages/loopover-miner/lib/governor-ledger-cli.js +++ /dev/null @@ -1,163 +0,0 @@ -import { runGovernorPause, runGovernorResume, runGovernorStatus } from "./governor-pause-cli.js"; -import { runGovernorMetrics } from "./governor-metrics-cli.js"; -/** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@loopover/engine`. */ -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([ - "allowed", - "denied", - "throttled", - "kill_switch", -]); -const GOVERNOR_LIST_USAGE = "Usage: loopover-miner governor list [--repo ] [--type allowed|denied|throttled|kill_switch] [--json]"; -const GOVERNOR_SUBCOMMAND_USAGE = [ - GOVERNOR_LIST_USAGE, - " loopover-miner governor pause [--reason ] [--dry-run] [--json]", - " loopover-miner governor resume [--dry-run] [--json]", - " loopover-miner governor status [--json]", - " loopover-miner governor metrics", -].join("\n"); -// The sole caller (the --repo branch of parseGovernorListArgs below) always checks `repoArg` is a -// truthy, non-flag-looking string before calling this, so `value` is never empty here. -function parseRepoArg(value) { - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - // #7525: extend #5831's path-safety guard to this CLI filter too — a `.`/`..`/control-char segment must not - // reach the ledger query. Reuse the same error shape as the malformed-input branch above (don't invent one). - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -export function parseGovernorListArgs(args) { - const options = { - json: false, - repoFullName: null, - type: null, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) - return { error: GOVERNOR_LIST_USAGE }; - const repo = parseRepoArg(repoArg); - if ("error" in repo) - return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token === "--type") { - const type = args[index + 1]; - if (!type || type.startsWith("-")) - return { error: GOVERNOR_LIST_USAGE }; - const trimmed = type.trim(); - if (!GOVERNOR_LEDGER_EVENT_TYPES.includes(trimmed)) { - return { - error: `Invalid type: ${trimmed}. Expected one of ${GOVERNOR_LEDGER_EVENT_TYPES.join(", ")}.`, - }; - } - options.type = trimmed; - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - positional.push(token); - } - if (positional.length > 0) - return { error: GOVERNOR_LIST_USAGE }; - return options; -} -export function filterGovernorEvents(events, options = {}) { - if (!Array.isArray(events)) - return []; - const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; - if (!type) - return events; - return events.filter((entry) => entry.eventType === type); -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderGovernorTable(events) { - if (!Array.isArray(events) || events.length === 0) - return "no governor ledger entries"; - const header = [ - "id".padStart(4), - "type".padEnd(12), - "repo".padEnd(24), - "action".padEnd(10), - "decision".padEnd(10), - "ts".padEnd(24), - ].join(" "); - const lines = events.map((entry) => [ - String(entry.id).padStart(4), - entry.eventType.padEnd(12), - display(entry.repoFullName).padEnd(24), - entry.actionClass.padEnd(10), - entry.decision.padEnd(10), - display(entry.ts).padEnd(24), - ].join(" ")); - return [header, ...lines].join("\n"); -} -async function withGovernorLedger(options, run) { - const ownsLedger = options.initGovernorLedger === undefined; - const initGovernorLedger = options.initGovernorLedger ?? (await import("./governor-ledger.js")).initGovernorLedger; - const governorLedger = initGovernorLedger(); - try { - return run(governorLedger); - } - finally { - if (ownsLedger) - governorLedger.close(); - } -} -export async function runGovernorList(args, options = {}) { - const parsed = parseGovernorListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return await withGovernorLedger(options, (governorLedger) => { - const events = filterGovernorEvents(governorLedger.readGovernorEvents({ - repoFullName: parsed.repoFullName, - }), { type: parsed.type }); - if (parsed.json) { - console.log(JSON.stringify({ events }, null, 2)); - } - else { - console.log(renderGovernorTable(events)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runGovernorCli(subcommand, args, options = {}) { - if (subcommand === "list") - return runGovernorList(args, options); - if (subcommand === "pause") - return runGovernorPause(args, options); - if (subcommand === "resume") - return runGovernorResume(args, options); - if (subcommand === "status") - return runGovernorStatus(args, options); - if (subcommand === "metrics") - return runGovernorMetrics(args, options); - return reportCliFailure(argsWantJson(args), `Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImdvdmVybm9yLWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFFakcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFL0Qsc0VBQXNFO0FBQ3RFLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUlyRCxNQUFNLDJCQUEyQixHQUF1QyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3BGLFNBQVM7SUFDVCxRQUFRO0lBQ1IsV0FBVztJQUNYLGFBQWE7Q0FDZCxDQUFDLENBQUM7QUFFSCxNQUFNLG1CQUFtQixHQUN2QixrSEFBa0gsQ0FBQztBQUVySCxNQUFNLHlCQUF5QixHQUFHO0lBQ2hDLG1CQUFtQjtJQUNuQiw2RUFBNkU7SUFDN0UsNERBQTREO0lBQzVELGdEQUFnRDtJQUNoRCx3Q0FBd0M7Q0FDekMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFpQmIsa0dBQWtHO0FBQ2xHLHVGQUF1RjtBQUN2RixTQUFTLFlBQVksQ0FBQyxLQUFhO0lBQ2pDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM3QixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQzNDLE9BQU8sRUFBRSxLQUFLLEVBQUUsd0NBQXdD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-ledger.d.ts b/packages/loopover-miner/lib/governor-ledger.d.ts deleted file mode 100644 index f8ab6d0ccb..0000000000 --- a/packages/loopover-miner/lib/governor-ledger.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -export type GovernorLedgerEntry = { - id: number; - ts: string; - eventType: string; - repoFullName: string | null; - actionClass: string; - decision: string; - reason: string; - payload: Record; -}; -export type AppendGovernorEventInput = { - eventType: string; - repoFullName?: string | null; - actionClass: string; - decision: string; - reason: string; - payload?: Record; -}; -export type ReadGovernorEventsFilter = { - repoFullName?: string | null; -}; -/** The public decision-log projection (#5159): every {@link GovernorLedgerEntry} field EXCEPT `payload`. */ -export type GovernorDecisionEntry = Omit; -export type GovernorLedger = { - dbPath: string; - appendGovernorEvent(event: AppendGovernorEventInput): GovernorLedgerEntry; - readGovernorEvents(filter?: ReadGovernorEventsFilter): GovernorLedgerEntry[]; - /** Read-only decision-log projection; excludes `payload` by construction (explicit named-column SELECT). */ - readGovernorDecisions(filter?: ReadGovernorEventsFilter): GovernorDecisionEntry[]; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare function resolveGovernorLedgerDbPath(env?: Record): string; -/** - * Opens the append-only governor ledger, creating the table on first use. Rows are returned in ascending `id` - * order (insertion order). (#2328) - */ -export declare function initGovernorLedger(dbPath?: string): GovernorLedger; -export declare function appendGovernorEvent(event: AppendGovernorEventInput): GovernorLedgerEntry; -export declare function readGovernorEvents(filter?: ReadGovernorEventsFilter): GovernorLedgerEntry[]; -export declare function closeDefaultGovernorLedger(): void; diff --git a/packages/loopover-miner/lib/governor-ledger.js b/packages/loopover-miner/lib/governor-ledger.js deleted file mode 100644 index c4ea88b350..0000000000 --- a/packages/loopover-miner/lib/governor-ledger.js +++ /dev/null @@ -1,167 +0,0 @@ -import { normalizeGovernorLedgerEvent } from "@loopover/engine"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { GOVERNOR_LEDGER_PURGE_SPEC, GOVERNOR_LEDGER_RETENTION_SPEC, purgeStoreByRepo, pruneLedgerByRetention, resolveLedgerRetentionPolicy, } from "./store-maintenance.js"; -const defaultDbFileName = "governor-ledger.sqlite3"; -let defaultGovernorLedger = null; -export function resolveGovernorLedgerDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_GOVERNOR_LEDGER_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveGovernorLedgerDbPath(), "invalid_governor_ledger_db_path"); -} -function normalizeOptionalRepoFullName(repoFullName) { - if (repoFullName === undefined || repoFullName === null) - return undefined; - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - // #7525: extend #5831's path-safety guard here too — reject a `.`/`..`/control-char segment before it can - // be persisted into SQLite (or echoed back through the CLI), matching claim-ledger.ts's sibling parser. - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function rowToEntry(row) { - let payload; - try { - payload = JSON.parse(row.payload_json); - if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { - throw new Error("corrupted_governor_row"); - } - } - catch { - throw new Error("corrupted_governor_row"); - } - return { - id: row.id, - ts: row.ts, - eventType: row.event_type, - repoFullName: row.repo_full_name, - actionClass: row.action_class, - decision: row.decision, - reason: row.reason, - payload: payload, - }; -} -// Decision-log projection (#5159): the public, MCP-exposed shape. Deliberately omits payload_json (which #5134 -// is expanding with reputation/self-plagiarism/budget state). Kept honest by an explicit named-column SELECT -// below — never SELECT * — so the sensitive column cannot leak even by accident. -function rowToDecision(row) { - return { - id: row.id, - ts: row.ts, - eventType: row.event_type, - repoFullName: row.repo_full_name, - actionClass: row.action_class, - decision: row.decision, - reason: row.reason, - }; -} -// v1 -> v2 (#4939/#6597): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of -// this same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing -// reads or writes it yet. Same defensive column-presence guard as this file's sibling stores' own additive -// migrations (e.g. event-ledger.js's addTenantIdColumn). -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(governor_events)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE governor_events ADD COLUMN tenant_id TEXT"); -} -function asGovernorDbRow(row) { - return row; -} -/** - * Opens the append-only governor ledger, creating the table on first use. Rows are returned in ascending `id` - * order (insertion order). (#2328) - */ -export function initGovernorLedger(dbPath = resolveGovernorLedgerDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS governor_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ts TEXT NOT NULL, - event_type TEXT NOT NULL, - repo_full_name TEXT, - action_class TEXT NOT NULL, - decision TEXT NOT NULL, - reason TEXT NOT NULL, - payload_json TEXT NOT NULL - ) - `); - db.exec("CREATE INDEX IF NOT EXISTS idx_governor_events_repo ON governor_events (repo_full_name, id)"); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addTenantIdColumn]); - // Opt-in retention (#4834): prune aged/excess rows when an operator has enabled it; a no-op by default. - pruneLedgerByRetention(db, GOVERNOR_LEDGER_RETENTION_SPEC, resolveLedgerRetentionPolicy(), Date.now()); - const appendStatement = db.prepare(` - INSERT INTO governor_events (ts, event_type, repo_full_name, action_class, decision, reason, payload_json) - VALUES (?, ?, ?, ?, ?, ?, ?) - `); - const getByIdStatement = db.prepare("SELECT * FROM governor_events WHERE id = ?"); - const readAllStatement = db.prepare("SELECT * FROM governor_events ORDER BY id ASC"); - const readByRepoStatement = db.prepare("SELECT * FROM governor_events WHERE repo_full_name = ? ORDER BY id ASC"); - // Explicit named-column projection for the read-only decision log (#5159) — payload_json is intentionally - // NOT in this list, so widening it would be a deliberate edit that the redaction test guards against. - const decisionColumns = "id, ts, event_type, repo_full_name, action_class, decision, reason"; - const readDecisionsAllStatement = db.prepare(`SELECT ${decisionColumns} FROM governor_events ORDER BY id ASC`); - const readDecisionsByRepoStatement = db.prepare(`SELECT ${decisionColumns} FROM governor_events WHERE repo_full_name = ? ORDER BY id ASC`); - return { - dbPath: resolvedPath, - appendGovernorEvent(event) { - const normalized = normalizeGovernorLedgerEvent(event); - const ts = new Date().toISOString(); - const result = appendStatement.run(ts, normalized.eventType, normalized.repoFullName, normalized.actionClass, normalized.decision, normalized.reason, normalized.payloadJson); - return rowToEntry(asGovernorDbRow(getByIdStatement.get(Number(result.lastInsertRowid)))); - }, - readGovernorEvents(filter = {}) { - const repoFullName = normalizeOptionalRepoFullName(filter.repoFullName); - const rows = repoFullName === undefined - ? readAllStatement.all() - : readByRepoStatement.all(repoFullName); - return rows.map((row) => rowToEntry(asGovernorDbRow(row))); - }, - readGovernorDecisions(filter = {}) { - const repoFullName = normalizeOptionalRepoFullName(filter.repoFullName); - const rows = repoFullName === undefined - ? readDecisionsAllStatement.all() - : readDecisionsByRepoStatement.all(repoFullName); - return rows.map((row) => rowToDecision(asGovernorDbRow(row))); - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. See the - // IMMUTABILITY INVARIANT note above: this is a deliberate, separate exception, not a normal ledger write. - // Requires a real repoFullName (unlike the optional filters above): a purge must never silently no-op. - purgeByRepo(repoFullName) { - const normalized = normalizeOptionalRepoFullName(repoFullName); - if (normalized === undefined) - throw new Error("invalid_repo_full_name"); - return purgeStoreByRepo(db, GOVERNOR_LEDGER_PURGE_SPEC, normalized); - }, - close() { - db.close(); - }, - }; -} -function getDefaultGovernorLedger() { - defaultGovernorLedger ??= initGovernorLedger(); - return defaultGovernorLedger; -} -export function appendGovernorEvent(event) { - return getDefaultGovernorLedger().appendGovernorEvent(event); -} -export function readGovernorEvents(filter) { - return getDefaultGovernorLedger().readGovernorEvents(filter); -} -export function closeDefaultGovernorLedger() { - if (!defaultGovernorLedger) - return; - defaultGovernorLedger.close(); - defaultGovernorLedger = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItbGVkZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZ292ZXJub3ItbGVkZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2hFLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxnQkFBZ0IsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3hHLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3JELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzVELE9BQU8sRUFDTCwwQkFBMEIsRUFDMUIsOEJBQThCLEVBQzlCLGdCQUFnQixFQUNoQixzQkFBc0IsRUFDdEIsNEJBQTRCLEdBQzdCLE1BQU0sd0JBQXdCLENBQUM7QUEwRGhDLE1BQU0saUJBQWlCLEdBQUcseUJBQXlCLENBQUM7QUFDcEQsSUFBSSxxQkFBcUIsR0FBMEIsSUFBSSxDQUFDO0FBRXhELE1BQU0sVUFBVSwyQkFBMkIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMvRixPQUFPLHVCQUF1QixDQUFDLGlCQUFpQixFQUFFLG1DQUFtQyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0FBQzlGLENBQUM7QUFFRCxTQUFTLGVBQWUsQ0FBQyxNQUFjO0lBQ3JDLE9BQU8seUJBQXlCLENBQUMsTUFBTSxFQUFFLDJCQUEyQixFQUFFLEVBQUUsaUNBQWlDLENBQUMsQ0FBQztBQUM3RyxDQUFDO0FBRUQsU0FBUyw2QkFBNkIsQ0FBQyxZQUF1QztJQUM1RSxJQUFJLFlBQVksS0FBSyxTQUFTLElBQUksWUFBWSxLQUFLLElBQUk7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUMxRSxJQUFJLE9BQU8sWUFBWSxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDaEYsTUFBTSxDQUFDLEtBQUssRUFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-metrics-cli.d.ts b/packages/loopover-miner/lib/governor-metrics-cli.d.ts deleted file mode 100644 index b0da4f7e23..0000000000 --- a/packages/loopover-miner/lib/governor-metrics-cli.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type GovernorCapUsage } from "@loopover/engine"; -import type { GovernorRateLimitState, GovernorState } from "./governor-state.js"; -export declare const GOVERNOR_RATE_LIMIT_REMAINING_RATIO = "loopover_miner_governor_rate_limit_remaining_ratio"; -export declare const GOVERNOR_CAP_USAGE_RATIO = "loopover_miner_governor_cap_usage_ratio"; -export type GovernorMetricsCliOptions = { - openGovernorState?: () => GovernorState; - nowMs?: number; -}; -export declare function renderGovernorMetrics(rateLimitState: GovernorRateLimitState, capUsage: GovernorCapUsage, nowMs: number): string; -export declare function runGovernorMetrics(args: string[], options?: GovernorMetricsCliOptions): Promise; diff --git a/packages/loopover-miner/lib/governor-metrics-cli.js b/packages/loopover-miner/lib/governor-metrics-cli.js deleted file mode 100644 index e2fde070d1..0000000000 --- a/packages/loopover-miner/lib/governor-metrics-cli.js +++ /dev/null @@ -1,146 +0,0 @@ -import { DEFAULT_AMS_POLICY_SPEC, DEFAULT_WRITE_RATE_LIMIT_POLICIES, evaluateGovernorCaps, evaluateLocalRateLimit, } from "@loopover/engine"; -import { openGovernorState } from "./governor-state.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -// `governor metrics` (#5187): render the governor's persisted rate-limit + cap-usage state (#5134, -// governor-state.js) as Prometheus text-exposition, so an operator's Alertmanager can page on rate-limit/ -// budget pressure without hand-rolling a scrape. Strictly read-only, mirroring queue-cli.js's `queue metrics` -// (#5186) and event-ledger-cli.js's `ledger metrics` (#4841): opens the local governor-state store, composes -// its EXISTING loadRateLimitState()/loadCapUsage() with the engine's already-exported PURE calculators -// (evaluateLocalRateLimit, evaluateGovernorCaps) against the SAME defaults the production loop (loop-cli.js) -// already falls back to when no `.loopover-ams.yml` override is configured (DEFAULT_WRITE_RATE_LIMIT_POLICIES, -// DEFAULT_AMS_POLICY_SPEC.capLimits) -- it never invents a threshold of its own, and it does not gate, retry, -// mutate, or otherwise touch governor decision logic (governor-chokepoint.js/governor-chokepoint-persisted.js -// are completely untouched by this file). -// -// capLimits is intentionally NOT read per-repo: governor-state.js's capUsage row is a single global scalar (a -// run-scoped cumulative counter, not indexed by repo -- see governor-state.js's own header comment), so a -// per-repo capLimits override from a resolved `.loopover-miner.yml` has no matching per-repo usage row to -// pair it with here. Using the fleet-wide DEFAULT_AMS_POLICY_SPEC.capLimits is the same approximation -// loop-cli.js itself already makes for any repo without its own override. -const GOVERNOR_METRICS_USAGE = "Usage: loopover-miner governor metrics"; -export const GOVERNOR_RATE_LIMIT_REMAINING_RATIO = "loopover_miner_governor_rate_limit_remaining_ratio"; -export const GOVERNOR_CAP_USAGE_RATIO = "loopover_miner_governor_cap_usage_ratio"; -/** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */ -function escapeMetricsHelpText(help) { - return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); -} -/** Prometheus label-value escaping — backslash, double-quote, newline (mirrors event-ledger-cli.js's - * escapeLabelValue). */ -function escapeLabelValue(value) { - return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); -} -/** buckets.perRepo is keyed by writeRateLimitRepoKey(actionClass, repoFullName) = "actionClass:repoFullName" - * (write-rate-limit.ts). actionClass is a fixed identifier (never contains ":"), so splitting on the FIRST - * colon recovers both parts even though repoFullName itself contains a "/". */ -function splitPerRepoKey(key) { - const separatorIndex = key.indexOf(":"); - if (separatorIndex === -1) - return { actionClass: key, repoFullName: "" }; - return { actionClass: key.slice(0, separatorIndex), repoFullName: key.slice(separatorIndex + 1) }; -} -// evaluateLocalRateLimit's own `remaining` field answers "how many MORE writes are allowed AFTER one more write -// right now" (rate-limit.ts: `remaining = allowed ? limit - effectiveCount - 1 : 0`) -- it is NOT current -// headroom. At count=2/limit=3 that field is already 0, identical to a fully exhausted count=3/limit=3 bucket, -// even though the count=2 bucket still has one write available. Recover true current headroom algebraically -// instead: when allowed, decision.remaining + 1 is exactly limit - effectiveCount (undo the "-1 for this next -// write" the decision already applied); when not allowed, headroom is 0. Every actionClass this loop reaches -// has already passed the DEFAULT_WRITE_RATE_LIMIT_POLICIES lookup above, so decision.limit is always one of the -// frozen, non-zero policy limits -- no zero-limit guard needed. -function remainingRatio(decision) { - const headroom = decision.allowed ? decision.remaining + 1 : 0; - return headroom / decision.limit; -} -function collectRateLimitRows(buckets, nowMs) { - const rows = []; - for (const [actionClass, bucket] of Object.entries(buckets.global)) { - const config = DEFAULT_WRITE_RATE_LIMIT_POLICIES.global[actionClass]; - if (!config) - continue; - rows.push({ - scope: "global", - actionClass, - repoFullName: "", - ratio: remainingRatio(evaluateLocalRateLimit(bucket, config, nowMs)), - }); - } - for (const [key, bucket] of Object.entries(buckets.perRepo)) { - const { actionClass, repoFullName } = splitPerRepoKey(key); - const config = DEFAULT_WRITE_RATE_LIMIT_POLICIES.perRepo[actionClass]; - if (!config) - continue; - rows.push({ - scope: "per_repo", - actionClass, - repoFullName, - ratio: remainingRatio(evaluateLocalRateLimit(bucket, config, nowMs)), - }); - } - rows.sort((a, b) => { - if (a.scope !== b.scope) - return a.scope.localeCompare(b.scope); - if (a.actionClass !== b.actionClass) - return a.actionClass.localeCompare(b.actionClass); - return a.repoFullName.localeCompare(b.repoFullName); - }); - return rows; -} -// DEFAULT_AMS_POLICY_SPEC.capLimits is a frozen, non-zero constant for every dimension -- no zero-limit guard -// needed, mirroring remainingRatio()'s reasoning above. -function collectCapUsageRows(capUsage) { - const report = evaluateGovernorCaps(capUsage, DEFAULT_AMS_POLICY_SPEC.capLimits); - return [ - { dimension: "budget", dimensionReport: report.budget }, - { dimension: "turns", dimensionReport: report.turns }, - { dimension: "elapsed_ms", dimensionReport: report.termination }, - ].map(({ dimension, dimensionReport }) => ({ - dimension, - ratio: dimensionReport.used / dimensionReport.limit, - })); -} -export function renderGovernorMetrics(rateLimitState, capUsage, nowMs) { - const rateLimitRows = collectRateLimitRows(rateLimitState.buckets, nowMs); - const capRows = collectCapUsageRows(capUsage); - const lines = [ - `# HELP ${GOVERNOR_RATE_LIMIT_REMAINING_RATIO} ${escapeMetricsHelpText("Remaining headroom in the governor's current write-rate-limit window, as a fraction of the configured limit (1 = empty bucket, 0 = exhausted). Evaluated against DEFAULT_WRITE_RATE_LIMIT_POLICIES.")}`, - `# TYPE ${GOVERNOR_RATE_LIMIT_REMAINING_RATIO} gauge`, - ]; - for (const row of rateLimitRows) { - const repoLabel = row.scope === "per_repo" ? `,repo="${escapeLabelValue(row.repoFullName)}"` : ""; - lines.push(`${GOVERNOR_RATE_LIMIT_REMAINING_RATIO}{scope="${row.scope}",action_class="${escapeLabelValue(row.actionClass)}"${repoLabel}} ${row.ratio}`); - } - lines.push(`# HELP ${GOVERNOR_CAP_USAGE_RATIO} ${escapeMetricsHelpText("The governor's persisted cumulative cap usage as a fraction of DEFAULT_AMS_POLICY_SPEC.capLimits (1 = ceiling reached). dimension is one of budget|turns|elapsed_ms.")}`); - lines.push(`# TYPE ${GOVERNOR_CAP_USAGE_RATIO} gauge`); - for (const row of capRows) { - lines.push(`${GOVERNOR_CAP_USAGE_RATIO}{dimension="${row.dimension}"} ${row.ratio}`); - } - return `${lines.join("\n")}\n`; -} -async function withGovernorState(options, run) { - const ownsGovernorState = options.openGovernorState === undefined; - const governorState = (options.openGovernorState ?? openGovernorState)(); - try { - return await run(governorState); - } - finally { - if (ownsGovernorState) - governorState.close(); - } -} -export async function runGovernorMetrics(args, options = {}) { - if (args.length > 0) { - return reportCliFailure(argsWantJson(args), GOVERNOR_METRICS_USAGE); - } - try { - return await withGovernorState(options, (governorState) => { - const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now(); - const rateLimitState = governorState.loadRateLimitState(); - const capUsage = governorState.loadCapUsage(); - console.log(renderGovernorMetrics(rateLimitState, capUsage, nowMs).trimEnd()); - return 0; - }); - } - catch (error) { - return reportCliFailure(argsWantJson(args), describeCliError(error)); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItbWV0cmljcy1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnb3Zlcm5vci1tZXRyaWNzLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsdUJBQXVCLEVBQ3ZCLGlDQUFpQyxFQUNqQyxvQkFBb0IsRUFDcEIsc0JBQXNCLEdBS3ZCLE1BQU0sa0JBQWtCLENBQUM7QUFDMUIsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFeEQsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWxGLG1HQUFtRztBQUNuRywwR0FBMEc7QUFDMUcsOEdBQThHO0FBQzlHLDZHQUE2RztBQUM3Ryx1R0FBdUc7QUFDdkcsNkdBQTZHO0FBQzdHLCtHQUErRztBQUMvRyw4R0FBOEc7QUFDOUcsOEdBQThHO0FBQzlHLDBDQUEwQztBQUMxQyxFQUFFO0FBQ0YsOEdBQThHO0FBQzlHLDBHQUEwRztBQUMxRywwR0FBMEc7QUFDMUcsc0dBQXNHO0FBQ3RHLDBFQUEwRTtBQUUxRSxNQUFNLHNCQUFzQixHQUFHLHdDQUF3QyxDQUFDO0FBRXhFLE1BQU0sQ0FBQyxNQUFNLG1DQUFtQyxHQUFHLG9EQUFvRCxDQUFDO0FBQ3hHLE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLHlDQUF5QyxDQUFDO0FBbUJsRix1R0FBdUc7QUFDdkcsU0FBUyxxQkFBcUIsQ0FBQyxJQUFZO0lBQ3pDLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUMsT0FBTyxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsQ0FBQztBQUMzRCxDQUFDO0FBRUQ7eUJBQ3lCO0FBQ3pCLFNBQVMsZ0JBQWdCLENBQUMsS0FBYTtJQUNyQyxPQUFPLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-pause-cli.d.ts b/packages/loopover-miner/lib/governor-pause-cli.d.ts deleted file mode 100644 index 7677e385d4..0000000000 --- a/packages/loopover-miner/lib/governor-pause-cli.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { GovernorState } from "./governor-state.js"; -export type ParsedGovernorPauseArgs = { - json: boolean; - dryRun: boolean; - reason: string | null; -} | { - error: string; -}; -export type ParsedGovernorResumeArgs = { - json: boolean; - dryRun: boolean; -} | { - error: string; -}; -export type ParsedGovernorNoArgsSubcommand = { - json: boolean; -} | { - error: string; -}; -export type GovernorPauseCliOptions = { - openGovernorState?: () => GovernorState; -}; -export declare function parseGovernorPauseArgs(args: string[]): ParsedGovernorPauseArgs; -export declare function parseGovernorResumeArgs(args: string[]): ParsedGovernorResumeArgs; -export declare function runGovernorPause(args: string[], options?: GovernorPauseCliOptions): Promise; -export declare function runGovernorResume(args: string[], options?: GovernorPauseCliOptions): Promise; -export declare function runGovernorStatus(args: string[], options?: GovernorPauseCliOptions): Promise; diff --git a/packages/loopover-miner/lib/governor-pause-cli.js b/packages/loopover-miner/lib/governor-pause-cli.js deleted file mode 100644 index 698b749f48..0000000000 --- a/packages/loopover-miner/lib/governor-pause-cli.js +++ /dev/null @@ -1,166 +0,0 @@ -// The governor pause/resume control surface (#4851): a real, persisted pause flag an operator (or, in a future -// wave, the governor itself) can toggle via this CLI, that loop-cli.js's iteration loop actually checks before -// each cycle. Distinct from governor-kill-switch.js (a read-only resolver over pre-existing env/YAML inputs this -// package never itself writes) and governor-run-halt.js (a one-way, run-scoped terminal breaker with no resume -// path) -- this is the first genuinely operator/governor-writable stop/go control. Persisted on governor-state.js's -// existing single-row scalar-state table, not a new store: a pause flag has no relational key of its own, the -// same reasoning that table's other scalar fields (rate-limit buckets, cap usage) already rely on. -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { openGovernorState } from "./governor-state.js"; -const GOVERNOR_PAUSE_USAGE = "Usage: loopover-miner governor pause [--reason ] [--dry-run] [--json]"; -const GOVERNOR_RESUME_USAGE = "Usage: loopover-miner governor resume [--dry-run] [--json]"; -const GOVERNOR_STATUS_USAGE = "Usage: loopover-miner governor status [--json]"; -export function parseGovernorPauseArgs(args) { - const options = { - json: false, - dryRun: false, - reason: null, - }; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what pausing would do and returns before writing to governor-state. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--reason") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: GOVERNOR_PAUSE_USAGE }; - options.reason = value; - index += 1; - continue; - } - return { error: `Unknown option: ${token}` }; - } - return options; -} -export function parseGovernorResumeArgs(args) { - const options = { json: false, dryRun: false }; - for (const token of args) { - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what resuming would do and returns before writing to governor-state. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - return { error: GOVERNOR_RESUME_USAGE }; - } - return options; -} -function parseNoArgsSubcommand(args, usage) { - if (args.length === 0) - return { json: false }; - if (args.length === 1 && args[0] === "--json") - return { json: true }; - return { error: usage }; -} -async function withGovernorState(options, run) { - const ownsGovernorState = options.openGovernorState === undefined; - const governorState = (options.openGovernorState ?? openGovernorState)(); - try { - return await run(governorState); - } - finally { - if (ownsGovernorState) - governorState.close(); - } -} -function renderPauseState(pauseState) { - if (!pauseState.paused) - return "governor is not paused"; - const reason = pauseState.reason ? ` (${pauseState.reason})` : ""; - return `governor is PAUSED since ${pauseState.pausedAt}${reason}`; -} -export async function runGovernorPause(args, options = {}) { - const parsed = parseGovernorPauseArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", paused: true, reason: parsed.reason }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult)); - } - else { - const reason = parsed.reason ? ` (${parsed.reason})` : ""; - console.log(`DRY RUN: would pause the governor${reason}. No governor-state write was made.`); - } - return 0; - } - try { - return await withGovernorState(options, (governorState) => { - const pauseState = governorState.savePauseState({ paused: true, reason: parsed.reason }); - if (parsed.json) { - console.log(JSON.stringify(pauseState)); - } - else { - console.log(renderPauseState(pauseState)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runGovernorResume(args, options = {}) { - const parsed = parseGovernorResumeArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", paused: false }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult)); - } - else { - console.log("DRY RUN: would resume the governor. No governor-state write was made."); - } - return 0; - } - try { - return await withGovernorState(options, (governorState) => { - const pauseState = governorState.savePauseState({ paused: false }); - if (parsed.json) { - console.log(JSON.stringify(pauseState)); - } - else { - console.log(renderPauseState(pauseState)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runGovernorStatus(args, options = {}) { - const parsed = parseNoArgsSubcommand(args, GOVERNOR_STATUS_USAGE); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return await withGovernorState(options, (governorState) => { - const pauseState = governorState.loadPauseState(); - if (parsed.json) { - console.log(JSON.stringify(pauseState)); - } - else { - console.log(renderPauseState(pauseState)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItcGF1c2UtY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZ292ZXJub3ItcGF1c2UtY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtHQUErRztBQUMvRywrR0FBK0c7QUFDL0csaUhBQWlIO0FBQ2pILCtHQUErRztBQUMvRyxvSEFBb0g7QUFDcEgsOEdBQThHO0FBQzlHLG1HQUFtRztBQUVuRyxPQUFPLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDbEYsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFHeEQsTUFBTSxvQkFBb0IsR0FBRyw2RUFBNkUsQ0FBQztBQUMzRyxNQUFNLHFCQUFxQixHQUFHLDREQUE0RCxDQUFDO0FBQzNGLE1BQU0scUJBQXFCLEdBQUcsZ0RBQWdELENBQUM7QUFjL0UsTUFBTSxVQUFVLHNCQUFzQixDQUFDLElBQWM7SUFDbkQsTUFBTSxPQUFPLEdBQThEO1FBQ3pFLElBQUksRUFBRSxLQUFLO1FBQ1gsTUFBTSxFQUFFLEtBQUs7UUFDYixNQUFNLEVBQUUsSUFBSTtLQUNiLENBQUM7SUFFRixLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDcEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzFCLElBQUksS0FBSyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3ZCLE9BQU8sQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1lBQ3BCLFNBQVM7UUFDWCxDQUFDO1FBQ0QscUZBQXFGO1FBQ3JGLElBQUksS0FBSyxLQUFLLFdBQVcsRUFBRSxDQUFDO1lBQzFCLE9BQU8sQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDO1lBQ3RCLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFL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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-run-halt.d.ts b/packages/loopover-miner/lib/governor-run-halt.d.ts deleted file mode 100644 index 2f619fc9ad..0000000000 --- a/packages/loopover-miner/lib/governor-run-halt.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type GovernorCapLimits, type GovernorCapUsage, type PortfolioConvergenceInput, type PortfolioConvergenceThresholds, type RunLoopHaltVerdict } from "@loopover/engine"; -import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js"; -import type { QueueEntry } from "./portfolio-queue.js"; -export type RunLoopInFlightItem = { - repoFullName: string; - identifier: string; -}; -export type EvaluateRunLoopBoundaryGateInput = { - runHalted?: boolean; - usage: GovernorCapUsage; - limits: GovernorCapLimits; - convergence: PortfolioConvergenceInput; - convergenceThresholds?: PortfolioConvergenceThresholds; - inFlightItem?: RunLoopInFlightItem | null; - markFailed?: (repoFullName: string, identifier: string) => QueueEntry | null; -}; -export type EvaluateRunLoopBoundaryGateResult = { - verdict: RunLoopHaltVerdict; - recorded: GovernorLedgerEntry | null; - runHalted: boolean; - canClaimNext: boolean; - releasedItem: QueueEntry | null; -}; -/** - * Evaluate run-loop halt signals before claiming the next portfolio item. - */ -export declare function evaluateRunLoopBoundaryGate(input: EvaluateRunLoopBoundaryGateInput, options?: { - append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry; -}): EvaluateRunLoopBoundaryGateResult; diff --git a/packages/loopover-miner/lib/governor-run-halt.js b/packages/loopover-miner/lib/governor-run-halt.js deleted file mode 100644 index a036687606..0000000000 --- a/packages/loopover-miner/lib/governor-run-halt.js +++ /dev/null @@ -1,39 +0,0 @@ -// Governor run-loop halt gate (#2347). Consults non-convergence + budget caps at each iteration boundary, -// releases in-flight portfolio items on a fresh halt, and records the decision to the governor ledger. -import { buildRunLoopHaltGovernorLedgerEvent, evaluateRunLoopHalt, } from "@loopover/engine"; -import { appendGovernorEvent } from "./governor-ledger.js"; -/** - * Evaluate run-loop halt signals before claiming the next portfolio item. - */ -export function evaluateRunLoopBoundaryGate(input, options = {}) { - const append = options.append ?? appendGovernorEvent; - const wasHalted = Boolean(input.runHalted); - const verdict = evaluateRunLoopHalt({ - runHalted: wasHalted, - usage: input.usage, - limits: input.limits, - convergence: input.convergence, - ...(input.convergenceThresholds !== undefined - ? { convergenceThresholds: input.convergenceThresholds } - : {}), - }); - const newlyHalted = !wasHalted && verdict.shouldHalt; - let releasedItem = null; - if (newlyHalted && input.inFlightItem && typeof input.markFailed === "function") { - releasedItem = input.markFailed(input.inFlightItem.repoFullName, input.inFlightItem.identifier); - } - const recorded = newlyHalted || (!wasHalted && !verdict.shouldHalt) - ? append( - // Engine ledger events allow explicit `undefined` on optional fields; the miner append - // contract uses exactOptionalPropertyTypes (`?: T` without `| undefined`). - buildRunLoopHaltGovernorLedgerEvent(input.inFlightItem?.repoFullName ?? null, input.inFlightItem?.identifier ?? null, verdict)) - : null; - return { - verdict, - recorded, - runHalted: verdict.shouldHalt, - canClaimNext: verdict.canClaimNext, - releasedItem, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItcnVuLWhhbHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnb3Zlcm5vci1ydW4taGFsdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwwR0FBMEc7QUFDMUcsdUdBQXVHO0FBRXZHLE9BQU8sRUFDTCxtQ0FBbUMsRUFDbkMsbUJBQW1CLEdBTXBCLE1BQU0sa0JBQWtCLENBQUM7QUFDMUIsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUEyQjNEOztHQUVHO0FBQ0gsTUFBTSxVQUFVLDJCQUEyQixDQUN6QyxLQUF1QyxFQUN2QyxVQUFpRixFQUFFO0lBRW5GLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxNQUFNLElBQUksbUJBQW1CLENBQUM7SUFDckQsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUMzQyxNQUFNLE9BQU8sR0FBRyxtQkFBbUIsQ0FBQztRQUNsQyxTQUFTLEVBQUUsU0FBUztRQUNwQixLQUFLLEVBQUUsS0FBSyxDQUFDLEtBQUs7UUFDbEIsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNO1FBQ3BCLFdBQVcsRUFBRSxLQUFLLENBQUMsV0FBVztRQUM5QixHQUFHLENBQUMsS0FBSyxDQUFDLHFCQUFxQixLQUFLLFNBQVM7WUFDM0MsQ0FBQyxDQUFDLEVBQUUscUJBQXFCLEVBQUUsS0FBSyxDQUFDLHFCQUFxQixFQUFFO1lBQ3hELENBQUMsQ0FBQyxFQUFFLENBQUM7S0FDUixDQUFDLENBQUM7SUFFSCxNQUFNLFdBQVcsR0FBRyxDQUFDLFNBQVMsSUFBSSxPQUFPLENBQUMsVUFBVSxDQUFDO0lBQ3JELElBQUksWUFBWSxHQUFzQixJQUFJLENBQUM7SUFDM0MsSUFBSSxXQUFXLElBQUksS0FBSyxDQUFDLFlBQVksSUFBSSxPQUFPLEtBQUssQ0FBQyxVQUFVLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDaEYsWUFBWSxHQUFHLEtBQUssQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxZQUFZLEVBQUUsS0FBSyxDQUFDLFlBQVksQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNsRyxDQUFDO0lBRUQsTUFBTSxRQUFRLEdBQ1osV0FBVyxJQUFJLENBQUMsQ0FBQyxTQUFTLElBQUksQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDO1FBQ2hELENBQUMsQ0FBQyxNQUFNO1FBQ0osdUZBQXVGO1FBQ3ZGLDJFQUEyRTtRQUMzRSxtQ0FBbUMsQ0FDakMsS0FBSyxDQUFDLFlBQVksRUFBRSxZQUFZLElBQUksSUFBSSxFQUN4QyxLQUFLLENBQUMsWUFBWSxFQUFFLFVBQVUsSUFBSSxJQUFJLEVBQ3RDLE9BQU8sQ0FDb0IsQ0FDOUI7UUFDSCxDQUFDLENBQUMsSUFBSSxDQUFDO0lBRVgsT0FBTztRQUNMLE9BQU87UUFDUCxRQUFRO1FBQ1IsU0FBUyxFQUFFLE9BQU8sQ0FBQyxVQUFVO1FBQzdCLFlBQVksRUFBRSxPQUFPLENBQUMsWUFBWTtRQUNsQyxZQUFZO0tBQ2IsQ0FBQztBQUNKLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-state.d.ts b/packages/loopover-miner/lib/governor-state.d.ts deleted file mode 100644 index 576f0e9a55..0000000000 --- a/packages/loopover-miner/lib/governor-state.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { GovernorCapUsage, OwnSubmissionRecord, RepoOutcomeHistory, WriteRateLimitBackoffStore, WriteRateLimitBucketStore } from "@loopover/engine"; -export type GovernorRateLimitState = { - buckets: WriteRateLimitBucketStore; - backoffAttempts: WriteRateLimitBackoffStore; -}; -export type ListRecentOwnSubmissionsFilter = { - repoFullName?: string; - limit?: number; -}; -export type GovernorPauseState = { - paused: boolean; - reason: string | null; - pausedAt: string | null; -}; -export type GovernorPauseInput = { - paused: boolean; - reason?: string | null; -}; -export type GovernorState = { - dbPath: string; - loadRateLimitState(): GovernorRateLimitState; - saveRateLimitState(rateLimitState: GovernorRateLimitState): void; - loadCapUsage(): GovernorCapUsage; - saveCapUsage(capUsage: GovernorCapUsage): void; - loadPauseState(): GovernorPauseState; - savePauseState(pauseState: GovernorPauseInput): GovernorPauseState; - loadReputationHistory(repoFullName: string, apiBaseUrl?: string): RepoOutcomeHistory; - saveReputationHistory(repoFullName: string, history: RepoOutcomeHistory, apiBaseUrl?: string): RepoOutcomeHistory; - recordOwnSubmission(record: OwnSubmissionRecord): OwnSubmissionRecord; - listRecentOwnSubmissions(filter?: ListRecentOwnSubmissionsFilter): OwnSubmissionRecord[]; - /** Delete every repo-scoped row for one repo across both governor tables (#7091); returns total rows removed. */ - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare function resolveGovernorStateDbPath(env?: Record): string; -/** Opens the local governor-state store, creating tables on first use. */ -export declare function openGovernorState(dbPath?: string): GovernorState; -export declare function loadRateLimitState(): GovernorRateLimitState; -export declare function saveRateLimitState(rateLimitState: GovernorRateLimitState): void; -export declare function loadCapUsage(): GovernorCapUsage; -export declare function saveCapUsage(capUsage: GovernorCapUsage): void; -export declare function loadPauseState(): GovernorPauseState; -export declare function savePauseState(pauseState: GovernorPauseInput): GovernorPauseState; -export declare function loadReputationHistory(repoFullName: string, apiBaseUrl?: string): RepoOutcomeHistory; -export declare function saveReputationHistory(repoFullName: string, history: RepoOutcomeHistory, apiBaseUrl?: string): RepoOutcomeHistory; -export declare function recordOwnSubmission(record: OwnSubmissionRecord): OwnSubmissionRecord; -export declare function listRecentOwnSubmissions(filter?: ListRecentOwnSubmissionsFilter): OwnSubmissionRecord[]; -export declare function closeDefaultGovernorState(): void; diff --git a/packages/loopover-miner/lib/governor-state.js b/packages/loopover-miner/lib/governor-state.js deleted file mode 100644 index dddbaeb04f..0000000000 --- a/packages/loopover-miner/lib/governor-state.js +++ /dev/null @@ -1,336 +0,0 @@ -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -import { GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, purgeStoreByRepo, } from "./store-maintenance.js"; -const defaultDbFileName = "governor-state.sqlite3"; -const DEFAULT_RATE_LIMIT_BUCKETS = Object.freeze({ global: {}, perRepo: {} }); -const DEFAULT_RATE_LIMIT_BACKOFF = Object.freeze({}); -const DEFAULT_CAP_USAGE = Object.freeze({ budgetSpent: 0, turnsTaken: 0, elapsedMs: 0 }); -const DEFAULT_REPUTATION_HISTORY = Object.freeze({ decided: 0, unfavorable: 0 }); -let defaultGovernorState = null; -export function resolveGovernorStateDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_GOVERNOR_STATE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveGovernorStateDbPath(), "invalid_governor_state_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - // #7525: extend #5831's path-safety guard here too — reject a `.`/`..`/control-char segment before it can - // be persisted into SQLite (or echoed back through the CLI), matching claim-ledger.ts's sibling parser. - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563). - * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */ -function normalizeApiBaseUrl(apiBaseUrl) { - if (apiBaseUrl === undefined || apiBaseUrl === null) - return DEFAULT_FORGE_CONFIG.apiBaseUrl; - if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) - throw new Error("invalid_api_base_url"); - return apiBaseUrl.trim(); -} -function parseJsonColumn(value, fallback) { - if (typeof value !== "string") - return fallback; - try { - const parsed = JSON.parse(value); - return parsed && typeof parsed === "object" ? parsed : fallback; - } - catch { - return fallback; - } -} -// Add the pause/resume columns (#4851) to an on-disk file created before they existed. `CREATE TABLE IF NOT -// EXISTS` above is a no-op against an already-existing table, so a pre-#4851 file needs this explicit ALTER -- -// guarded by a per-column presence check (rather than a single `paused`-only check) so a file that somehow -// has `paused` but not `pause_reason`/`paused_at` still gets the columns it's missing, same technique as -// portfolio-queue.js's own post-creation column migration. -function ensurePauseColumns(db) { - const existingColumns = new Set(db - .prepare("PRAGMA table_info(governor_scalar_state)") - .all() - .map((column) => column.name)); - if (!existingColumns.has("paused")) { - db.exec("ALTER TABLE governor_scalar_state ADD COLUMN paused INTEGER NOT NULL DEFAULT 0"); - } - if (!existingColumns.has("pause_reason")) { - db.exec("ALTER TABLE governor_scalar_state ADD COLUMN pause_reason TEXT"); - } - if (!existingColumns.has("paused_at")) { - db.exec("ALTER TABLE governor_scalar_state ADD COLUMN paused_at TEXT"); - } -} -// Rebuild governor_reputation_history's bare `repo_full_name` PRIMARY KEY into a (api_base_url, repo_full_name) -// composite (#5563) -- two forge hosts serving a same-named owner/repo must not share one reputation row. -// SQLite cannot ALTER a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy every -// existing row with the pre-#4784 implicit single-forge default backfilled, drop the old table, rename the new -// one in. Guarded by a column-presence check (matching ensurePauseColumns' idempotence) so this only runs once -// per file, same technique as portfolio-queue.js's own post-creation migration. -function ensureReputationHistoryForgeScope(db) { - const hasApiBaseUrlColumn = db - .prepare("PRAGMA table_info(governor_reputation_history)") - .all() - .some((column) => column.name === "api_base_url"); - if (hasApiBaseUrlColumn) - return; - db.exec(` - CREATE TABLE governor_reputation_history_v2 ( - api_base_url TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - decided INTEGER NOT NULL, - unfavorable INTEGER NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY (api_base_url, repo_full_name) - ) - `); - // OR IGNORE: a source row that somehow violates the rebuilt table's NOT NULL columns (a hand-edited or - // otherwise corrupted file) is skipped rather than aborting the whole migration -- same fail-closed posture - // as run-state.js's own #5563 migration. - db.prepare(`INSERT OR IGNORE INTO governor_reputation_history_v2 (api_base_url, repo_full_name, decided, unfavorable, updated_at) - SELECT ?, repo_full_name, decided, unfavorable, updated_at FROM governor_reputation_history`).run(DEFAULT_FORGE_CONFIG.apiBaseUrl); - db.exec("DROP TABLE governor_reputation_history"); - db.exec("ALTER TABLE governor_reputation_history_v2 RENAME TO governor_reputation_history"); -} -/** Opens the local governor-state store, creating tables on first use. */ -export function openGovernorState(dbPath = resolveGovernorStateDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - // ONE row (id=1) holding the whole-run scalar state: rate-limit buckets/backoff and budget/turn/termination - // usage have no natural per-repo key of their own beyond what's already encoded inside the JSON blob - // (WriteRateLimitBucketStore.perRepo is itself keyed by `${actionClass}:${repoFullName}`), so a single - // UPSERTed row is simpler and more honest than inventing a relational key that doesn't exist upstream. - db.exec(` - CREATE TABLE IF NOT EXISTS governor_scalar_state ( - id INTEGER PRIMARY KEY CHECK (id = 1), - rate_limit_buckets_json TEXT NOT NULL, - rate_limit_backoff_json TEXT NOT NULL, - cap_usage_json TEXT NOT NULL, - paused INTEGER NOT NULL DEFAULT 0, - pause_reason TEXT, - paused_at TEXT, - updated_at TEXT NOT NULL - ) - `); - ensurePauseColumns(db); - db.exec(` - CREATE TABLE IF NOT EXISTS governor_reputation_history ( - repo_full_name TEXT PRIMARY KEY, - decided INTEGER NOT NULL, - unfavorable INTEGER NOT NULL, - updated_at TEXT NOT NULL - ) - `); - ensureReputationHistoryForgeScope(db); - db.exec(` - CREATE TABLE IF NOT EXISTS governor_own_submissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_full_name TEXT NOT NULL, - fingerprint TEXT NOT NULL, - submitted_at TEXT, - pull_request_number INTEGER, - issue_number INTEGER - ) - `); - db.exec("CREATE INDEX IF NOT EXISTS idx_governor_own_submissions_repo ON governor_own_submissions (repo_full_name, id)"); - const getScalarStatement = db.prepare("SELECT * FROM governor_scalar_state WHERE id = 1"); - const upsertScalarStatement = db.prepare(` - INSERT INTO governor_scalar_state - (id, rate_limit_buckets_json, rate_limit_backoff_json, cap_usage_json, paused, pause_reason, paused_at, updated_at) - VALUES (1, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET - rate_limit_buckets_json = excluded.rate_limit_buckets_json, - rate_limit_backoff_json = excluded.rate_limit_backoff_json, - cap_usage_json = excluded.cap_usage_json, - paused = excluded.paused, - pause_reason = excluded.pause_reason, - paused_at = excluded.paused_at, - updated_at = excluded.updated_at - `); - const getReputationStatement = db.prepare("SELECT * FROM governor_reputation_history WHERE api_base_url = ? AND repo_full_name = ?"); - const upsertReputationStatement = db.prepare(` - INSERT INTO governor_reputation_history (api_base_url, repo_full_name, decided, unfavorable, updated_at) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(api_base_url, repo_full_name) DO UPDATE SET - decided = excluded.decided, - unfavorable = excluded.unfavorable, - updated_at = excluded.updated_at - `); - const insertSubmissionStatement = db.prepare(` - INSERT INTO governor_own_submissions (repo_full_name, fingerprint, submitted_at, pull_request_number, issue_number) - VALUES (?, ?, ?, ?, ?) - `); - const listSubmissionsAllStatement = db.prepare("SELECT * FROM governor_own_submissions ORDER BY id DESC LIMIT ?"); - const listSubmissionsByRepoStatement = db.prepare("SELECT * FROM governor_own_submissions WHERE repo_full_name = ? ORDER BY id DESC LIMIT ?"); - function rowToSubmission(row) { - return { - repoFullName: row.repo_full_name, - fingerprint: row.fingerprint, - submittedAt: row.submitted_at, - pullRequestNumber: row.pull_request_number, - issueNumber: row.issue_number, - }; - } - // BEGIN IMMEDIATE takes the write lock BEFORE `fn`'s read, so two processes on the same file (the loop daemon - // saving rate-limit/cap-usage state on every gated write, and an operator's `governor pause`/`resume` CLI - // invocation racing it) cannot interleave a stale read with each other's write and silently clobber the - // scalar-state column-group they don't own -- same fix shape as event-ledger.js's appendEvent (#7221). Shared - // by all three governor_scalar_state save methods below, since they all read-then-write across the same row. - function withTransaction(fn) { - db.exec("BEGIN IMMEDIATE"); - try { - const result = fn(); - db.exec("COMMIT"); - return result; - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - } - const state = { - dbPath: resolvedPath, - loadRateLimitState() { - const row = getScalarStatement.get(); - return { - buckets: parseJsonColumn(row?.rate_limit_buckets_json, DEFAULT_RATE_LIMIT_BUCKETS), - backoffAttempts: parseJsonColumn(row?.rate_limit_backoff_json, DEFAULT_RATE_LIMIT_BACKOFF), - }; - }, - saveRateLimitState(rateLimitState) { - withTransaction(() => { - const row = getScalarStatement.get(); - upsertScalarStatement.run(JSON.stringify(rateLimitState?.buckets ?? DEFAULT_RATE_LIMIT_BUCKETS), JSON.stringify(rateLimitState?.backoffAttempts ?? DEFAULT_RATE_LIMIT_BACKOFF), row ? row.cap_usage_json : JSON.stringify(DEFAULT_CAP_USAGE), row ? row.paused : 0, row ? row.pause_reason : null, row ? row.paused_at : null, new Date().toISOString()); - }); - }, - loadCapUsage() { - const row = getScalarStatement.get(); - return parseJsonColumn(row?.cap_usage_json, DEFAULT_CAP_USAGE); - }, - saveCapUsage(capUsage) { - withTransaction(() => { - const row = getScalarStatement.get(); - upsertScalarStatement.run(row ? row.rate_limit_buckets_json : JSON.stringify(DEFAULT_RATE_LIMIT_BUCKETS), row ? row.rate_limit_backoff_json : JSON.stringify(DEFAULT_RATE_LIMIT_BACKOFF), JSON.stringify(capUsage ?? DEFAULT_CAP_USAGE), row ? row.paused : 0, row ? row.pause_reason : null, row ? row.paused_at : null, new Date().toISOString()); - }); - }, - // The governor pause/resume control surface (#4851): a real, persisted, operator/governor-writable flag the - // loop checks before each cycle -- distinct from governor-kill-switch.js (a read-only resolver over env/YAML - // inputs the miner does not itself write) and governor-run-halt.js (a one-way, run-scoped terminal breaker). - // `pausedAt` is stamped fresh on every transition INTO paused, and cleared on resume, so a status query can - // report how long a pause has been in effect without needing a separate history table. - loadPauseState() { - const row = getScalarStatement.get(); - return { - paused: row ? Boolean(row.paused) : false, - reason: row?.pause_reason ?? null, - pausedAt: row?.paused_at ?? null, - }; - }, - savePauseState(pauseState) { - const paused = Boolean(pauseState?.paused); - const reason = typeof pauseState?.reason === "string" && pauseState.reason.trim() ? pauseState.reason.trim() : null; - const pausedAt = paused ? new Date().toISOString() : null; - withTransaction(() => { - const row = getScalarStatement.get(); - upsertScalarStatement.run(row ? row.rate_limit_buckets_json : JSON.stringify(DEFAULT_RATE_LIMIT_BUCKETS), row ? row.rate_limit_backoff_json : JSON.stringify(DEFAULT_RATE_LIMIT_BACKOFF), row ? row.cap_usage_json : JSON.stringify(DEFAULT_CAP_USAGE), paused ? 1 : 0, reason, pausedAt, new Date().toISOString()); - }); - return { paused, reason, pausedAt }; - }, - loadReputationHistory(repoFullName, apiBaseUrl) { - const normalizedForge = normalizeApiBaseUrl(apiBaseUrl); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const row = getReputationStatement.get(normalizedForge, normalizedRepo); - if (!row) - return { ...DEFAULT_REPUTATION_HISTORY }; - return { decided: row.decided, unfavorable: row.unfavorable }; - }, - saveReputationHistory(repoFullName, history, apiBaseUrl) { - const normalizedForge = normalizeApiBaseUrl(apiBaseUrl); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const decided = Number.isInteger(history?.decided) ? history.decided : 0; - const unfavorable = Number.isInteger(history?.unfavorable) ? history.unfavorable : 0; - upsertReputationStatement.run(normalizedForge, normalizedRepo, decided, unfavorable, new Date().toISOString()); - return { decided, unfavorable }; - }, - recordOwnSubmission(record) { - const normalized = normalizeRepoFullName(record?.repoFullName); - if (typeof record?.fingerprint !== "string" || !record.fingerprint.trim()) { - throw new Error("invalid_fingerprint"); - } - const submittedAt = typeof record.submittedAt === "string" ? record.submittedAt : new Date().toISOString(); - const pullRequestNumber = Number.isInteger(record.pullRequestNumber) ? record.pullRequestNumber : null; - const issueNumber = Number.isInteger(record.issueNumber) ? record.issueNumber : null; - insertSubmissionStatement.run(normalized, record.fingerprint, submittedAt, pullRequestNumber, issueNumber); - return { repoFullName: normalized, fingerprint: record.fingerprint, submittedAt, pullRequestNumber, issueNumber }; - }, - listRecentOwnSubmissions(filter = {}) { - const limit = Number.isInteger(filter.limit) && filter.limit > 0 ? filter.limit : 200; - const rows = filter.repoFullName === undefined - ? listSubmissionsAllStatement.all(limit) - : listSubmissionsByRepoStatement.all(normalizeRepoFullName(filter.repoFullName), limit); - return rows.map((row) => rowToSubmission(row)); - }, - /** - * Delete every repo-scoped row for one repo across BOTH governor tables against this single open handle - * (#7091) — the right-to-be-forgotten path `loopover-miner purge` invokes. `governor_reputation_history` is - * purged on `repo_full_name` alone (its key is composite with `api_base_url`), so nothing survives on any - * forge host. `governor_scalar_state` is deliberately untouched — it has no repo dimension. Returns the - * total rows removed across both tables. - */ - purgeByRepo(repoFullName) { - const normalized = normalizeRepoFullName(repoFullName); - return (purgeStoreByRepo(db, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, normalized) + - purgeStoreByRepo(db, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, normalized)); - }, - close() { - db.close(); - }, - }; - return state; -} -function getDefaultGovernorState() { - defaultGovernorState ??= openGovernorState(); - return defaultGovernorState; -} -export function loadRateLimitState() { - return getDefaultGovernorState().loadRateLimitState(); -} -export function saveRateLimitState(rateLimitState) { - return getDefaultGovernorState().saveRateLimitState(rateLimitState); -} -export function loadCapUsage() { - return getDefaultGovernorState().loadCapUsage(); -} -export function saveCapUsage(capUsage) { - return getDefaultGovernorState().saveCapUsage(capUsage); -} -export function loadPauseState() { - return getDefaultGovernorState().loadPauseState(); -} -export function savePauseState(pauseState) { - return getDefaultGovernorState().savePauseState(pauseState); -} -export function loadReputationHistory(repoFullName, apiBaseUrl) { - return getDefaultGovernorState().loadReputationHistory(repoFullName, apiBaseUrl); -} -export function saveReputationHistory(repoFullName, history, apiBaseUrl) { - return getDefaultGovernorState().saveReputationHistory(repoFullName, history, apiBaseUrl); -} -export function recordOwnSubmission(record) { - return getDefaultGovernorState().recordOwnSubmission(record); -} -export function listRecentOwnSubmissions(filter) { - return getDefaultGovernorState().listRecentOwnSubmissions(filter); -} -export function closeDefaultGovernorState() { - if (!defaultGovernorState) - return; - defaultGovernorState.close(); - defaultGovernorState = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3Itc3RhdGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnb3Zlcm5vci1zdGF0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFRQSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUN6RCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN4RyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNyRCxPQUFPLEVBQ0wsbUNBQW1DLEVBQ25DLHNDQUFzQyxFQUN0QyxnQkFBZ0IsR0FDakIsTUFBTSx3QkFBd0IsQ0FBQztBQXdGaEMsTUFBTSxpQkFBaUIsR0FBRyx3QkFBd0IsQ0FBQztBQUNuRCxNQUFNLDBCQUEwQixHQUF3QyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQztBQUNuSCxNQUFNLDBCQUEwQixHQUF5QyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQzNGLE1BQU0saUJBQWlCLEdBQStCLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDckgsTUFBTSwwQkFBMEIsR0FBaUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFDLEVBQUUsV0FBVyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDL0csSUFBSSxvQkFBb0IsR0FBeUIsSUFBSSxDQUFDO0FBRXRELE1BQU0sVUFBVSwwQkFBMEIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUM5RixPQUFPLHVCQUF1QixDQUFDLGlCQUFpQixFQUFFLGtDQUFrQyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/harness-submission-trigger.d.ts b/packages/loopover-miner/lib/harness-submission-trigger.d.ts deleted file mode 100644 index 9111d9b86b..0000000000 --- a/packages/loopover-miner/lib/harness-submission-trigger.d.ts +++ /dev/null @@ -1,121 +0,0 @@ -export declare const HARNESS_SUBMISSION_TRIGGER_DECISION_EVENT: "harness_submission_trigger_decision"; -export type HarnessSubmissionSlopBand = "clean" | "low" | "elevated" | "high"; -export type HarnessSubmissionMode = "observe" | "enforce"; -export type HarnessSubmissionKillSwitchScope = "global" | "repo" | "none"; -export type HarnessSubmissionCandidateInput = { - /** Forwarded to shouldSubmit's own kill-switch check (#2339). */ - killSwitchScope: HarnessSubmissionKillSwitchScope; - repoFullName: string; - handoffPacket: { - worktreePath: string; - branchRef?: string; - diffSummary: string; - selfReviewVerdict: unknown; - attemptLogReference: string; - }; - slopThreshold: HarnessSubmissionSlopBand; - mode: HarnessSubmissionMode; - maxConsecutiveGateBlocks?: number; -}; -export interface HarnessSubmissionEventLedger { - appendEvent(event: { - type: string; - repoFullName?: string; - payload: Record; - }): { - id: number; - seq: number; - type: string; - repoFullName: string | null; - payload: Record; - createdAt: string; - }; - readEvents(filter?: { - since?: number; - repoFullName?: string; - }): Array<{ - type: string; - repoFullName?: string | null; - payload?: Record; - createdAt: string; - }>; -} -export type HarnessSubmissionDeps = { - eventLedger: HarnessSubmissionEventLedger; - sessionStartMs?: number; -}; -export type HarnessSubmissionDecision = { - allow: boolean; - reasons: string[]; - circuitBreakerTripped: boolean; -}; -export type HarnessSubmissionResult = { - decision: HarnessSubmissionDecision; - event: { - id: number; - seq: number; - type: string; - repoFullName: string | null; - payload: Record; - createdAt: string; - }; -}; -/** Count consecutive `allow: false` decisions recorded at or after `sinceMs`, walking backward from the most - * recent decision until an `allow: true` breaks the streak (or history runs out). Session-scoped (not - * filtered by repo) to match the circuit breaker's own "pauses the run entirely" semantics. */ -export declare function countConsecutiveGateBlocks(eventLedger: HarnessSubmissionEventLedger, sinceMs: number): number; -/** - * Evaluate the harness submission trigger for one candidate handoff, reading real session history to compute - * the circuit-breaker tally, and always appending exactly one audit event. Fails closed (throws) on a - * malformed candidate or missing required dependency. - * - * @param {{ killSwitchScope: "global"|"repo"|"none", repoFullName: string, handoffPacket: object, slopThreshold: "clean"|"low"|"elevated"|"high", mode: "observe"|"enforce", maxConsecutiveGateBlocks?: number }} candidate - * @param {{ eventLedger: object, sessionStartMs?: number }} deps - */ -export declare function evaluateAndRecordHarnessSubmissionTrigger(candidate: HarnessSubmissionCandidateInput, deps: HarnessSubmissionDeps): HarnessSubmissionResult; -/** The exact input shape buildOpenPrSpec (`@loopover/engine`) expects. */ -export type OpenPrInput = { - repoFullName: string; - base: string; - head: string; - title: string; - body: string; - draft: boolean; -}; -export type PrepareOpenPrSubmissionCandidate = HarnessSubmissionCandidateInput & { - base: string; - title: string; - body?: string; - draft?: boolean; -}; -export type PrepareOpenPrSubmissionResult = { - ready: true; - decision: HarnessSubmissionDecision; - event: HarnessSubmissionResult["event"]; - openPrInput: OpenPrInput; -} | { - ready: false; - decision: HarnessSubmissionDecision; - event: HarnessSubmissionResult["event"]; -}; -/** - * Bridge one completed handoff through the submission gate to a submission-READY payload -- the exact input - * shape `buildOpenPrSpec` (`@loopover/engine`) expects (repoFullName/base/head/title/body/draft). On `allow: - * true` returns `{ ready: true, decision, event, openPrInput }`; otherwise `{ ready: false, decision, event }` - * -- the block reasons are on `decision.reasons` and already on the ledger via the wrapped call either way. - * Does NOT call `buildOpenPrSpec` itself: this stays a gate→payload bridge; `attempt-runner.js` (and MCP - * `loopover_open_pr` equivalents) take `openPrInput` from a `ready: true` result and call - * `buildOpenPrSpec`. The cross-package "unreachable from root src/" reason no longer applies (#5131/#5132 - * moved the builder into `@loopover/engine`), but the deliberate non-call layering is still necessary. - * - * Fails closed (throws) on a malformed candidate, mirroring evaluateAndRecordHarnessSubmissionTrigger's own - * validation -- a missing PR title/base is a caller bug that must never silently degrade into a garbage spec. - * The one field evaluateAndRecordHarnessSubmissionTrigger does NOT itself require -- handoffPacket.branchRef, - * optional there because iterate-loop.ts deliberately does not manage worktrees/branches -- IS required here, - * but only once the decision is known to be `allow: true`: a PR cannot be opened without a source branch, but a - * blocked candidate needs no branch at all, and must not throw for a reason unrelated to why it was blocked. - * - * @param {{ killSwitchScope: "global"|"repo"|"none", repoFullName: string, handoffPacket: { branchRef?: string, [key: string]: unknown }, slopThreshold: "clean"|"low"|"elevated"|"high", mode: "observe"|"enforce", maxConsecutiveGateBlocks?: number, base: string, title: string, body?: string, draft?: boolean }} candidate - * @param {{ eventLedger: object, sessionStartMs?: number }} deps - */ -export declare function prepareOpenPrSubmission(candidate: PrepareOpenPrSubmissionCandidate, deps: HarnessSubmissionDeps): PrepareOpenPrSubmissionResult; diff --git a/packages/loopover-miner/lib/harness-submission-trigger.js b/packages/loopover-miner/lib/harness-submission-trigger.js deleted file mode 100644 index cf161cd45a..0000000000 --- a/packages/loopover-miner/lib/harness-submission-trigger.js +++ /dev/null @@ -1,137 +0,0 @@ -import { evaluateHarnessSubmissionTrigger } from "@loopover/engine"; -// Harness submission-gate wiring orchestrator (#2337): the real-IO half of connecting the gated-submission -// decision (`shouldSubmit`, wrapped by `evaluateHarnessSubmissionTrigger`, @loopover/engine) to a -// real driving loop's own handoff signal. Reads the session's recent decision history to compute the -// consecutive-block circuit-breaker tally, consults the pure decision, and always records exactly one audit -// event -- regardless of outcome, so a paused-pending-human-review session leaves a full trail of why. -// -// NOT WIRED INTO ANY AUTOMATIC SCHEDULE: per this issue's own "manual owner sign-off on the wiring before this -// ships to any default-on profile" deliverable. `prepareOpenPrSubmission` below is the gate→payload bridge: -// on `allow: true` it shapes the exact input `buildOpenPrSpec` (`@loopover/engine`, -// `packages/loopover-engine/src/miner/local-write-tools.ts`, re-exported from the engine public barrel) expects -// as `openPrInput`. It deliberately does NOT call `buildOpenPrSpec` itself -- that stays the caller's job so -// this module stays a decision-to-payload bridge. The in-package caller is `attempt-runner.js`, which imports -// `buildOpenPrSpec` from `@loopover/engine` and runs it after a `ready: true` result (the pre-#5131/#5132 -// "unreachable from root `src/mcp/`" boundary no longer applies, but the layering still does: gate evaluate → -// shape openPrInput here → build the runnable local-write spec in the driver). Equivalent MCP call sites -// (e.g. `loopover_open_pr`) can likewise take `openPrInput` from a `ready: true` result. -// -// SESSION-SCOPED, NOT PER-REPO: the circuit breaker's own "pauses the run entirely" wording means the tally is -// counted across EVERY repo's decisions this session, not scoped to one repo -- distinct from #2338's loop- -// reentry circuit breaker, which is deliberately per-repo (a rejection streak on one repo must not pause -// unrelated repos). -export const HARNESS_SUBMISSION_TRIGGER_DECISION_EVENT = "harness_submission_trigger_decision"; -/** Count consecutive `allow: false` decisions recorded at or after `sinceMs`, walking backward from the most - * recent decision until an `allow: true` breaks the streak (or history runs out). Session-scoped (not - * filtered by repo) to match the circuit breaker's own "pauses the run entirely" semantics. */ -export function countConsecutiveGateBlocks(eventLedger, sinceMs) { - const decisions = eventLedger - .readEvents({}) - .filter((event) => event.type === HARNESS_SUBMISSION_TRIGGER_DECISION_EVENT && Date.parse(event.createdAt) >= sinceMs); - let count = 0; - for (let i = decisions.length - 1; i >= 0; i -= 1) { - if (decisions[i].payload?.allow === true) - break; - count += 1; - } - return count; -} -/** - * Evaluate the harness submission trigger for one candidate handoff, reading real session history to compute - * the circuit-breaker tally, and always appending exactly one audit event. Fails closed (throws) on a - * malformed candidate or missing required dependency. - * - * @param {{ killSwitchScope: "global"|"repo"|"none", repoFullName: string, handoffPacket: object, slopThreshold: "clean"|"low"|"elevated"|"high", mode: "observe"|"enforce", maxConsecutiveGateBlocks?: number }} candidate - * @param {{ eventLedger: object, sessionStartMs?: number }} deps - */ -export function evaluateAndRecordHarnessSubmissionTrigger(candidate, deps) { - if (!candidate || typeof candidate !== "object") - throw new Error("invalid_harness_submission_candidate"); - if (!["global", "repo", "none"].includes(candidate.killSwitchScope)) - throw new Error("invalid_kill_switch_scope"); - const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : ""; - if (!repoFullName) - throw new Error("invalid_repo_full_name"); - if (!candidate.handoffPacket || typeof candidate.handoffPacket !== "object") - throw new Error("invalid_handoff_packet"); - if (!deps || typeof deps !== "object") - throw new Error("invalid_harness_submission_deps"); - const { eventLedger, sessionStartMs = 0 } = deps; - if (!eventLedger || typeof eventLedger.appendEvent !== "function" || typeof eventLedger.readEvents !== "function") { - throw new Error("invalid_event_ledger"); - } - const consecutiveGateBlocks = countConsecutiveGateBlocks(eventLedger, sessionStartMs); - const decision = evaluateHarnessSubmissionTrigger({ - killSwitchScope: candidate.killSwitchScope, - handoffPacket: candidate.handoffPacket, - slopThreshold: candidate.slopThreshold, - mode: candidate.mode, - consecutiveGateBlocks, - maxConsecutiveGateBlocks: candidate.maxConsecutiveGateBlocks, - }); - const event = eventLedger.appendEvent({ - type: HARNESS_SUBMISSION_TRIGGER_DECISION_EVENT, - repoFullName, - payload: { - killSwitchScope: candidate.killSwitchScope, - allow: decision.allow, - reasons: decision.reasons, - circuitBreakerTripped: decision.circuitBreakerTripped, - consecutiveGateBlocks, - attemptLogReference: candidate.handoffPacket.attemptLogReference ?? null, - }, - }); - return { decision, event }; -} -/** - * Bridge one completed handoff through the submission gate to a submission-READY payload -- the exact input - * shape `buildOpenPrSpec` (`@loopover/engine`) expects (repoFullName/base/head/title/body/draft). On `allow: - * true` returns `{ ready: true, decision, event, openPrInput }`; otherwise `{ ready: false, decision, event }` - * -- the block reasons are on `decision.reasons` and already on the ledger via the wrapped call either way. - * Does NOT call `buildOpenPrSpec` itself: this stays a gate→payload bridge; `attempt-runner.js` (and MCP - * `loopover_open_pr` equivalents) take `openPrInput` from a `ready: true` result and call - * `buildOpenPrSpec`. The cross-package "unreachable from root src/" reason no longer applies (#5131/#5132 - * moved the builder into `@loopover/engine`), but the deliberate non-call layering is still necessary. - * - * Fails closed (throws) on a malformed candidate, mirroring evaluateAndRecordHarnessSubmissionTrigger's own - * validation -- a missing PR title/base is a caller bug that must never silently degrade into a garbage spec. - * The one field evaluateAndRecordHarnessSubmissionTrigger does NOT itself require -- handoffPacket.branchRef, - * optional there because iterate-loop.ts deliberately does not manage worktrees/branches -- IS required here, - * but only once the decision is known to be `allow: true`: a PR cannot be opened without a source branch, but a - * blocked candidate needs no branch at all, and must not throw for a reason unrelated to why it was blocked. - * - * @param {{ killSwitchScope: "global"|"repo"|"none", repoFullName: string, handoffPacket: { branchRef?: string, [key: string]: unknown }, slopThreshold: "clean"|"low"|"elevated"|"high", mode: "observe"|"enforce", maxConsecutiveGateBlocks?: number, base: string, title: string, body?: string, draft?: boolean }} candidate - * @param {{ eventLedger: object, sessionStartMs?: number }} deps - */ -export function prepareOpenPrSubmission(candidate, deps) { - if (!candidate || typeof candidate !== "object") - throw new Error("invalid_harness_submission_candidate"); - const base = typeof candidate.base === "string" ? candidate.base.trim() : ""; - if (!base) - throw new Error("invalid_pr_base"); - const title = typeof candidate.title === "string" ? candidate.title.trim() : ""; - if (!title) - throw new Error("invalid_pr_title"); - const { decision, event } = evaluateAndRecordHarnessSubmissionTrigger(candidate, deps); - if (!decision.allow) - return { ready: false, decision, event }; - // Only reached once evaluateAndRecordHarnessSubmissionTrigger has already validated handoffPacket is a - // well-formed object -- safe to read .branchRef directly. - const head = typeof candidate.handoffPacket.branchRef === "string" ? candidate.handoffPacket.branchRef.trim() : ""; - if (!head) - throw new Error("invalid_pr_head_branch"); - return { - ready: true, - decision, - event, - openPrInput: { - repoFullName: candidate.repoFullName.trim(), - base, - head, - title, - body: typeof candidate.body === "string" ? candidate.body : "", - draft: candidate.draft === true, - }, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGFybmVzcy1zdWJtaXNzaW9uLXRyaWdnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJoYXJuZXNzLXN1Ym1pc3Npb24tdHJpZ2dlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsZ0NBQWdDLEVBQXNCLE1BQU0sa0JBQWtCLENBQUM7QUFFeEYsMkdBQTJHO0FBQzNHLGtHQUFrRztBQUNsRyxxR0FBcUc7QUFDckcsNEdBQTRHO0FBQzVHLHVHQUF1RztBQUN2RyxFQUFFO0FBQ0YsK0dBQStHO0FBQy9HLDRHQUE0RztBQUM1RyxvRkFBb0Y7QUFDcEYsZ0hBQWdIO0FBQ2hILDZHQUE2RztBQUM3Ryw4R0FBOEc7QUFDOUcsMEdBQTBHO0FBQzFHLDhHQUE4RztBQUM5Ryx5R0FBeUc7QUFDekcseUZBQXlGO0FBQ3pGLEVBQUU7QUFDRiwrR0FBK0c7QUFDL0csNEdBQTRHO0FBQzVHLHlHQUF5RztBQUN6RyxvQkFBb0I7QUFFcEIsTUFBTSxDQUFDLE1BQU0seUNBQXlDLEdBQUcscUNBQThDLENBQUM7QUEyQ3hHOztnR0FFZ0c7QUFDaEcsTUFBTSxVQUFVLDBCQUEwQixDQUFDLFdBQXlDLEVBQUUsT0FBZTtJQUNuRyxNQUFNLFNBQVMsR0FBRyxXQUFXO1NBQzFCLFVBQVUsQ0FBQyxFQUFFLENBQUM7U0FDZCxNQUFNLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUsseUNBQXlDLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLElBQUksT0FBTyxDQUFDLENBQUM7SUFDekgsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsS0FBSyxJQUFJLENBQUMsR0FBRyxTQUFTLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUNsRCxJQUFJLFNBQVMsQ0FBQyxDQUFDLENBQUUsQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/http-retry.d.ts b/packages/loopover-miner/lib/http-retry.d.ts deleted file mode 100644 index c056b49e7f..0000000000 --- a/packages/loopover-miner/lib/http-retry.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type FetchWithRetryOptions = { - maxAttempts?: number; - sleepFn?: (ms: number) => Promise; - backoffMs?: (attempt: number) => number; - timeoutMs?: number; -}; -/** Exponential backoff from a base delay, capped: attempt 1 → base, 2 → 2×base, 3 → 4×base, … ≤ MAX_BACKOFF_MS. */ -export declare function defaultRetryBackoffMs(attempt: number): number; -/** - * Perform `fetchFn(url, init)` with bounded retry on a transient 5xx OR rate-limit (429 / secondary-403) response. - * A retryable status is retried (sleeping `Retry-After` or `backoffMs(attempt)`, whichever is longer, between - * attempts) up to `maxAttempts`; any other 2xx/3xx/4xx response is returned immediately, and after the last attempt - * a lingering retryable status is returned as-is (the caller's own error handling still runs). A THROWN - * error is NOT retried — it propagates to the caller (the pollers' #4281 failure-mode contract). When `timeoutMs` - * is given, each attempt gets its own fresh abort timeout (a stalled connection is exactly the kind of network- - * level failure #4281 already bubbles unretried, so a timed-out attempt propagates the same way). - */ -export declare function fetchWithRetry(fetchFn: (url: unknown, init?: unknown) => Promise, url: unknown, init?: unknown, options?: FetchWithRetryOptions): Promise; diff --git a/packages/loopover-miner/lib/http-retry.js b/packages/loopover-miner/lib/http-retry.js deleted file mode 100644 index 8172ee0a58..0000000000 --- a/packages/loopover-miner/lib/http-retry.js +++ /dev/null @@ -1,98 +0,0 @@ -// Bounded retry-with-backoff around a single HTTP call (#4829). The miner's pollers (ci-poller and others) -// previously let a single brief 5xx from GitHub kill the whole poll loop, because their own attempt loop -// only re-polls while a conclusion is genuinely "pending", never after a server error. This wraps ONE fetch so a -// transient SERVER error (a 5xx RESPONSE) or a transient GitHub RATE-LIMIT response (429 / secondary-403, #6761) -// is retried a bounded number of times, DISTINCT from that pending-polling, sleeping an exponential backoff (or -// the response's `Retry-After`, whichever is longer) between attempts and giving up after `maxAttempts`. Any other -// 2xx/3xx/4xx response — including a plain permission 403 — is returned immediately, and a THROWN error (a network- -// level failure) propagates unchanged rather than being retried — the pollers' existing failure-mode contract -// (#4281) deliberately bubbles those to the caller. -// Pure control flow over injected `fetchFn`/`sleepFn`/`backoffMs` — no real network or timers in tests. -const DEFAULT_MAX_ATTEMPTS = 3; -const DEFAULT_BASE_BACKOFF_MS = 500; -const MAX_BACKOFF_MS = 10_000; -/** Clamp `maxAttempts` to a positive integer, flooring BEFORE the positivity test so a fractional value below 1 - * falls back to the default rather than becoming a 0 that would skip every attempt. */ -function normalizeMaxAttempts(raw) { - const numeric = Math.floor(Number(raw)); - return Number.isFinite(numeric) && numeric >= 1 ? numeric : DEFAULT_MAX_ATTEMPTS; -} -/** Exponential backoff from a base delay, capped: attempt 1 → base, 2 → 2×base, 3 → 4×base, … ≤ MAX_BACKOFF_MS. */ -export function defaultRetryBackoffMs(attempt) { - return Math.min(MAX_BACKOFF_MS, DEFAULT_BASE_BACKOFF_MS * 2 ** (Math.max(1, attempt) - 1)); -} -const defaultSleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); -/** Read a response header defensively — works with a real `Headers` object or a test stub exposing `.get()`. */ -function readHeader(response, name) { - const headers = response && response.headers; - return headers && typeof headers.get === "function" ? headers.get(name) : null; -} -/** - * A transient GitHub rate-limit response the poll should ride out rather than abort on (#6761): a 429 (primary - * rate limit / abuse), or a SECONDARY-rate-limit 403 — identified by a `Retry-After` header or `x-ratelimit- - * remaining: 0`. A plain permission-denied 403 carries neither signal and is deliberately NOT treated as a rate - * limit: it can never succeed, so retrying it would only burn the bounded attempt budget. - */ -function isRateLimitStatus(response) { - if (response.status === 429) - return true; - if (response.status !== 403) - return false; - if (readHeader(response, "retry-after") != null) - return true; - const remaining = readHeader(response, "x-ratelimit-remaining"); - return remaining != null && Number(remaining) === 0; -} -/** Retry a transient SERVER error (5xx) OR a transient rate-limit response (429 / secondary-403). (#6761) */ -function isRetryableStatus(response) { - return response.status >= 500 || isRateLimitStatus(response); -} -/** - * Delay before the next attempt. Honor a `Retry-After` header (delta-seconds) when GitHub sends one — but never - * below the computed exponential backoff (so a tiny/zero value can't hammer) and never above MAX_BACKOFF_MS; - * otherwise fall back to the exponential backoff alone. (#6761) - */ -function retryDelayMs(response, attempt, backoffMs) { - const base = backoffMs(attempt); - const retryAfterSeconds = Number(readHeader(response, "retry-after")); - if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) { - return Math.min(MAX_BACKOFF_MS, Math.max(base, retryAfterSeconds * 1000)); - } - return base; -} -/** - * Perform `fetchFn(url, init)` with bounded retry on a transient 5xx OR rate-limit (429 / secondary-403) response. - * A retryable status is retried (sleeping `Retry-After` or `backoffMs(attempt)`, whichever is longer, between - * attempts) up to `maxAttempts`; any other 2xx/3xx/4xx response is returned immediately, and after the last attempt - * a lingering retryable status is returned as-is (the caller's own error handling still runs). A THROWN - * error is NOT retried — it propagates to the caller (the pollers' #4281 failure-mode contract). When `timeoutMs` - * is given, each attempt gets its own fresh abort timeout (a stalled connection is exactly the kind of network- - * level failure #4281 already bubbles unretried, so a timed-out attempt propagates the same way). - */ -export async function fetchWithRetry(fetchFn, url, init, options = {}) { - const maxAttempts = normalizeMaxAttempts(options.maxAttempts); - const sleepFn = typeof options.sleepFn === "function" ? options.sleepFn : defaultSleep; - const backoffMs = typeof options.backoffMs === "function" ? options.backoffMs : defaultRetryBackoffMs; - for (let attempt = 1;; attempt += 1) { - // A thrown error is intentionally NOT caught here — it propagates to the caller unchanged. - const response = await fetchOnce(fetchFn, url, init, options.timeoutMs); - // Retry transient SERVER errors (5xx) AND transient GitHub rate-limit responses (429 / secondary-403, #6761). - // Everything else (2xx/3xx/other 4xx incl. a plain permission 403) is returned immediately; on the final - // attempt a lingering retryable status is returned as-is so the caller's own error handling still runs. - if (!isRetryableStatus(response) || attempt >= maxAttempts) - return response; - await sleepFn(retryDelayMs(response, attempt, backoffMs)); - } -} -// A fresh AbortSignal.timeout() per attempt, never one shared across retries -- reusing a single signal would -// leave every attempt after the first pre-aborted the instant it fired once. AbortSignal.timeout()'s own internal -// timer is unref'd (verified: it never keeps a short-lived CLI process alive past its own work), so unlike a raw -// setTimeout it needs no manual clearTimeout -- mirrors src/github/client.ts's timeoutFetch in the main repo. A -// no-op passthrough (no `init` copy) when `timeoutMs` is absent/non-positive, so every existing caller that -// doesn't opt in sees zero behavior change. -function fetchOnce(fetchFn, url, init, timeoutMs) { - if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) - return fetchFn(url, init); - return fetchFn(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cC1yZXRyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImh0dHAtcmV0cnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsMkdBQTJHO0FBQzNHLHlHQUF5RztBQUN6RyxpSEFBaUg7QUFDakgsaUhBQWlIO0FBQ2pILGdIQUFnSDtBQUNoSCxtSEFBbUg7QUFDbkgsb0hBQW9IO0FBQ3BILDhHQUE4RztBQUM5RyxvREFBb0Q7QUFDcEQsd0dBQXdHO0FBRXhHLE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxDQUFDO0FBQy9CLE1BQU0sdUJBQXVCLEdBQUcsR0FBRyxDQUFDO0FBQ3BDLE1BQU0sY0FBYyxHQUFHLE1BQU0sQ0FBQztBQWM5Qjt3RkFDd0Y7QUFDeEYsU0FBUyxvQkFBb0IsQ0FBQyxHQUFZO0lBQ3hDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDeEMsT0FBTyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxJQUFJLE9BQU8sSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsb0JBQW9CLENBQUM7QUFDbkYsQ0FBQztBQUVELG1IQUFtSDtBQUNuSCxNQUFNLFVBQVUscUJBQXFCLENBQUMsT0FBZTtJQUNuRCxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsY0FBYyxFQUFFLHVCQUF1QixHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDN0YsQ0FBQztBQUVELE1BQU0sWUFBWSxHQUFHLENBQUMsT0FBZSxFQUFpQixFQUFFLENBQUMsSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztBQUVoSCxnSEFBZ0g7QUFDaEgsU0FBUyxV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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/idea-feasibility-cli.d.ts b/packages/loopover-miner/lib/idea-feasibility-cli.d.ts deleted file mode 100644 index c2b86727fc..0000000000 --- a/packages/loopover-miner/lib/idea-feasibility-cli.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AssessIdeaFeasibilityOptions } from "./idea-feasibility.js"; -import type { FeasibilityClaimStatus, FeasibilityDuplicateClusterRisk } from "@loopover/engine"; -export type ParsedIdeaFeasibilityArgs = { - claimStatus: FeasibilityClaimStatus; - duplicateClusterRisk: FeasibilityDuplicateClusterRisk; - targetResolvable: boolean; - acceptanceHints: string[]; - json: boolean; -} | { - error: string; -}; -export declare function parseIdeaFeasibilityArgs(args: string[]): ParsedIdeaFeasibilityArgs; -export type RunIdeaFeasibilityCliOptions = AssessIdeaFeasibilityOptions; -export declare function runIdeaFeasibilityCli(args: string[], options?: RunIdeaFeasibilityCliOptions): number; diff --git a/packages/loopover-miner/lib/idea-feasibility-cli.js b/packages/loopover-miner/lib/idea-feasibility-cli.js deleted file mode 100644 index cf18ecb46c..0000000000 --- a/packages/loopover-miner/lib/idea-feasibility-cli.js +++ /dev/null @@ -1,87 +0,0 @@ -/** `idea-feasibility` CLI command: the freeform-idea counterpart to the metadata `feasibility` CLI - * (feasibility-cli.js, #4270). It runs a freeform Rent-a-Loop idea submission (#4779) through the - * pre-compute feasibility gate (idea-feasibility.js, #5671) so a renter can no longer burn compute on an - * idea that was never going to succeed — the same parse -> execute -> render wrapper the metadata gate uses, - * only the idea's `issueStatus` is DERIVED from its own structure rather than supplied. Purely local — no - * network, no filesystem — so it never needs the npm-registry update check other subcommands opt into. */ -import { assessIdeaFeasibility } from "./idea-feasibility.js"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -const CLAIM_STATUSES = ["unclaimed", "claimed", "solved", "unknown"]; -const DUPLICATE_CLUSTER_RISKS = ["none", "low", "medium", "high"]; -/** Plain `Array.includes` doesn't narrow a `string` argument down to the array's literal element type, so this - * small type-guard wrapper does it explicitly wherever a parsed CLI token needs to become one of these enums. */ -function isOneOf(value, allowed) { - return allowed.includes(value); -} -const IDEA_FEASIBILITY_USAGE = "Usage: loopover-miner idea-feasibility [--not-resolvable] [--hint ]... [--json]\n" + - ` claimStatus: ${CLAIM_STATUSES.join("|")}\n` + - ` duplicateClusterRisk: ${DUPLICATE_CLUSTER_RISKS.join("|")}\n` + - " --not-resolvable: the idea's target repo does not resolve to a repo the loop can act on (issueStatus=missing)\n" + - " --hint : an objective acceptance signal (repeatable); an idea declaring none is invalid (issueStatus=invalid)"; -export function parseIdeaFeasibilityArgs(args) { - const options = { json: false, targetResolvable: true, acceptanceHints: [] }; - const positional = []; - for (let i = 0; i < args.length; i += 1) { - const token = args[i]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--not-resolvable") { - options.targetResolvable = false; - continue; - } - if (token === "--hint") { - const value = args[i + 1]; - // A whitespace-only hint is as empty as a missing one (#6766): it declares no testable success signal, so - // it gets the same rejection rather than sailing through as a real objective signal. - if (value === undefined || value.startsWith("-") || value.trim() === "") { - return { error: "--hint requires a value." }; - } - options.acceptanceHints.push(value); - i += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 2) { - return { error: IDEA_FEASIBILITY_USAGE }; - } - // positional.length === 2 was just verified above, so this tuple cast is safe. - const [claimStatus, duplicateClusterRisk] = positional; - if (!isOneOf(claimStatus, CLAIM_STATUSES)) { - return { error: `claimStatus must be one of: ${CLAIM_STATUSES.join(", ")}.` }; - } - if (!isOneOf(duplicateClusterRisk, DUPLICATE_CLUSTER_RISKS)) { - return { error: `duplicateClusterRisk must be one of: ${DUPLICATE_CLUSTER_RISKS.join(", ")}.` }; - } - return { - claimStatus, - duplicateClusterRisk, - targetResolvable: options.targetResolvable, - acceptanceHints: options.acceptanceHints, - json: options.json, - }; -} -export function runIdeaFeasibilityCli(args, options = {}) { - const parsed = parseIdeaFeasibilityArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const assessment = assessIdeaFeasibility({ acceptanceHints: parsed.acceptanceHints }, { - targetResolvable: parsed.targetResolvable, - claimStatus: parsed.claimStatus, - duplicateClusterRisk: parsed.duplicateClusterRisk, - }, options); - if (parsed.json) { - console.log(JSON.stringify(assessment, null, 2)); - } - else { - console.log(`${assessment.disposition}: ${assessment.summary}`); - } - return 0; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlYS1mZWFzaWJpbGl0eS1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpZGVhLWZlYXNpYmlsaXR5LWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7MEdBSzBHO0FBQzFHLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRTlELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUdoRSxNQUFNLGNBQWMsR0FBRyxDQUFDLFdBQVcsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBc0QsQ0FBQztBQUMxSCxNQUFNLHVCQUF1QixHQUFHLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsTUFBTSxDQUErRCxDQUFDO0FBRWhJO2tIQUNrSDtBQUNsSCxTQUFTLE9BQU8sQ0FBbUIsS0FBYSxFQUFFLE9BQXFCO0lBQ3JFLE9BQVEsT0FBNkIsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDeEQsQ0FBQztBQUVELE1BQU0sc0JBQXNCLEdBQzFCLDhIQUE4SDtJQUM5SCxrQkFBa0IsY0FBYyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSTtJQUM5QywyQkFBMkIsdUJBQXVCLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxJQUFJO0lBQ2hFLG1IQUFtSDtJQUNuSCx1SEFBdUgsQ0FBQztBQVkxSCxNQUFNLFVBQVUsd0JBQXdCLENBQUMsSUFBYztJQUNyRCxNQUFNLE9BQU8sR0FBRyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsZ0JBQWdCLEVBQUUsSUFBSSxFQUFFLGVBQWUsRUFBRSxFQUFjLEVBQUUsQ0FBQztJQUN6RixNQUFNLFVBQVUsR0FBYSxFQUFFLENBQUM7SUFFaEMsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/idea-feasibility.d.ts b/packages/loopover-miner/lib/idea-feasibility.d.ts deleted file mode 100644 index e9fafcdba1..0000000000 --- a/packages/loopover-miner/lib/idea-feasibility.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { FeasibilityClaimStatus, FeasibilityDuplicateClusterRisk, FeasibilityGateInput, FeasibilityGateResult, FeasibilityIssueStatus, FeasibilityVerdict } from "@loopover/engine"; -/** A schema-validated idea submission (#4779). This structural gate only reads `acceptanceHints`, but accepts - * the full submission so callers can pass the idea through unchanged. */ -export type IdeaFeasibilityInput = { - title?: string | undefined; - body?: string | undefined; - targetRepo?: string | undefined; - constraints?: readonly string[] | undefined; - acceptanceHints?: readonly string[] | undefined; - priority?: "normal" | "high" | undefined; -}; -/** Objectively-resolved intake signals for the idea (resolved by the caller, never guessed from prose). */ -export type ResolvedIdeaSignals = { - targetResolvable: boolean; - claimStatus: FeasibilityClaimStatus; - duplicateClusterRisk: FeasibilityDuplicateClusterRisk; -}; -export type AssessIdeaFeasibilityOptions = { - buildFeasibilityVerdict?: (input: FeasibilityGateInput) => FeasibilityGateResult; -}; -export type IdeaFeasibilityDisposition = "proceed" | "flag" | "reject"; -export type IdeaFeasibilityResult = { - disposition: IdeaFeasibilityDisposition; - verdict: FeasibilityVerdict; - issueStatus: FeasibilityIssueStatus; - reasons: string[]; - summary: string; -}; -/** - * Derive the feasibility `issueStatus` for a freeform idea from objective, structural signals only — never from - * a semantic read of the prose. - */ -export declare function deriveIdeaIssueStatus(idea: IdeaFeasibilityInput, resolved: Pick): FeasibilityIssueStatus; -/** - * Assess a schema-validated idea's feasibility before compute is allocated. - */ -export declare function assessIdeaFeasibility(idea: IdeaFeasibilityInput, resolved: ResolvedIdeaSignals, options?: AssessIdeaFeasibilityOptions): IdeaFeasibilityResult; diff --git a/packages/loopover-miner/lib/idea-feasibility.js b/packages/loopover-miner/lib/idea-feasibility.js deleted file mode 100644 index 03cf768a2f..0000000000 --- a/packages/loopover-miner/lib/idea-feasibility.js +++ /dev/null @@ -1,65 +0,0 @@ -/** Pre-execution feasibility check for a freeform Rent-a-Loop idea (#5671). - * - * Runs post-schema-validation and pre-compute-allocation on an idea submission (the intake shape defined in - * #4779), so a customer can no longer burn paid or free-trial compute on an idea that was never going to - * succeed. It is the freeform-text counterpart to the metadata `feasibility` CLI (`feasibility-cli.js`, #4270). - * - * REUSED from feasibility-cli.js AS-IS: - * - the engine's pure `buildFeasibilityVerdict` composer and its `avoid > raise > go` precedence — an idea - * inherits exactly the same verdict machinery a metadata-resolved issue does, so there is no second, - * divergent decision surface; - * - the injectable-verdict test seam (`options.buildFeasibilityVerdict`), matching the CLI's convention. - * - * NEW for freeform text (#5671, per the #4779 rubric): - * - `deriveIdeaIssueStatus`, which computes the `issueStatus` discriminant from the idea's OWN structure - * instead of a resolved GitHub issue. An idea with no objective success signal is `invalid` (impossible to - * evaluate objectively) and is rejected before compute; an unresolvable target repo is `missing` (out of the - * loop's scope) and is flagged. - * - * OUT OF SCOPE (stays with #5136): judging abusive/illegal or semantically off-topic intent from prose — that is - * a content-moderation policy call, not this deterministic structural gate. - */ -import { buildFeasibilityVerdict } from "@loopover/engine"; -/** Verdict → caller-facing disposition. `go` proceeds to compute; `raise`/`avoid` gate it. */ -const DISPOSITION_BY_VERDICT = { - go: "proceed", - raise: "flag", - avoid: "reject", -}; -/** - * Derive the feasibility `issueStatus` for a freeform idea from objective, structural signals only — never from - * a semantic read of the prose. - */ -export function deriveIdeaIssueStatus(idea, resolved) { - // Out of the loop's scope: the idea does not resolve to a repo the loop can act on. - if (!resolved.targetResolvable) - return "missing"; - // Impossible to evaluate objectively: no declared success signal, so the loop could never test its own output. - // Count CONTENT, not array length (#6766): a blank/whitespace-only hint declares nothing testable, so it must - // not pass as an objective signal just by occupying a slot. - const objectiveSignals = (idea.acceptanceHints ?? []).filter((hint) => typeof hint === "string" && hint.trim() !== "").length; - if (objectiveSignals === 0) - return "invalid"; - return "ready"; -} -/** - * Assess a schema-validated idea's feasibility before compute is allocated. - */ -export function assessIdeaFeasibility(idea, resolved, options = {}) { - const buildVerdict = options.buildFeasibilityVerdict ?? buildFeasibilityVerdict; - const issueStatus = deriveIdeaIssueStatus(idea, resolved); - const verdict = buildVerdict({ - found: resolved.targetResolvable, - claimStatus: resolved.claimStatus, - duplicateClusterRisk: resolved.duplicateClusterRisk, - issueStatus, - }); - return { - disposition: DISPOSITION_BY_VERDICT[verdict.verdict], - verdict: verdict.verdict, - issueStatus, - reasons: [...verdict.avoidReasons, ...verdict.raiseReasons], - summary: verdict.summary, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlYS1mZWFzaWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImlkZWEtZmVhc2liaWxpdHkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0JHO0FBQ0gsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUEwQzNELDhGQUE4RjtBQUM5RixNQUFNLHNCQUFzQixHQUEyRDtJQUNyRixFQUFFLEVBQUUsU0FBUztJQUNiLEtBQUssRUFBRSxNQUFNO0lBQ2IsS0FBSyxFQUFFLFFBQVE7Q0FDaEIsQ0FBQztBQUVGOzs7R0FHRztBQUNILE1BQU0sVUFBVSxxQkFBcUIsQ0FDbkMsSUFBMEIsRUFDMUIsUUFBdUQ7SUFFdkQsb0ZBQW9GO0lBQ3BGLElBQUksQ0FBQyxRQUFRLENBQUMsZ0JBQWdCO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDakQsK0dBQStHO0lBQy9HLDhHQUE4RztJQUM5Ryw0REFBNEQ7SUFDNUQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLElBQUksQ0FBQyxlQUFlLElBQUksRUFBRSxDQUFDLENBQUMsTUFBTSxDQUMxRCxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQ3pELENBQUMsTUFBTSxDQUFDO0lBQ1QsSUFBSSxnQkFBZ0IsS0FBSyxDQUFDO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDN0MsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxVQUFVLHFCQUFxQixDQUNuQyxJQUEwQixFQUMxQixRQUE2QixFQUM3QixVQUF3QyxFQUFFO0lBRTFDLE1BQU0sWUFBWSxHQUFHLE9BQU8sQ0FBQyx1QkFBdUIsSUFBSSx1QkFBdUIsQ0FBQztJQUNoRixNQUFNLFdBQVcsR0FBRyxxQkFBcUIsQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDMUQsTUFBTSxPQUFPLEdBQUcsWUFBWSxDQUFDO1FBQzNCLEtBQUssRUFBRSxRQUFRLENBQUMsZ0JBQWdCO1FBQ2hDLFdBQVcsRUFBRSxRQUFRLENBQUMsV0FBVztRQUNqQyxvQkFBb0IsRUFBRSxRQUFRLENBQUMsb0JBQW9CO1FBQ25ELFdBQVc7S0FDWixDQUFDLENBQUM7SUFDSCxPQUFPO1FBQ0wsV0FBVyxFQUFFLHNCQUFzQixDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUM7UUFDcEQsT0FBTyxFQUFFLE9BQU8sQ0FBQyxPQUFPO1FBQ3hCLFdBQVc7UUFDWCxPQUFPLEVBQUUsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxZQUFZLEVBQUUsR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDO1FBQzNELE9BQU8sRUFBRSxPQUFPLENBQUMsT0FBTztLQUN6QixDQUFDO0FBQ0osQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/init-wizard.d.ts b/packages/loopover-miner/lib/init-wizard.d.ts deleted file mode 100644 index 5035b8ff72..0000000000 --- a/packages/loopover-miner/lib/init-wizard.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -export type WizardIo = { - promptText(question: string): Promise; - promptMasked(question: string): Promise; - writeLine(text: string): void; - close?: () => void; - /** Whether this `io`'s underlying input is a real, interactive terminal. Optional -- a fake test `io` that - * omits it is treated as interactive (matching every pre-#6846 test's existing behavior); `createWizardIo`'s - * real adapter always sets it from the actual stream's `isTTY`. */ - isInteractive?: boolean; -}; -export type RunInteractiveInitOptions = { - /** DI override for the device-flow's outbound fetch calls (tests never touch the real network). */ - fetchImpl?: typeof fetch; - /** DI override for the device-flow poll loop's sleep between requests (tests never wait on a real timer). */ - sleepFn?: (ms: number) => Promise; -}; -/** Where the wizard writes its starter .env file: the miner state dir, the same directory `init` already uses - * for laptop-state.sqlite3. */ -export declare function resolveWizardEnvFilePath(env?: Record): string; -/** Render collected `[KEY, value]` pairs as sourceable `KEY=value` lines, one per entry, insertion order. Pure - * and filesystem-free so it is directly testable. */ -export declare function renderWizardEnvFile(entries: ReadonlyArray): string; -/** - * Menu selection sourced from the engine's own `CODING_AGENT_DRIVER_NAMES`, so the choices can never drift from - * what the driver factory actually resolves. Empty input SKIPS provider selection entirely (leaves - * MINER_CODING_AGENT_PROVIDER unwritten, deferring to whatever default the CLI already resolves) -- distinct - * from explicitly choosing the `noop` entry. - */ -export declare function promptProviderSelection(io: WizardIo): Promise; -/** - * Optional, skippable per-provider companion vars (model override / timeout), sourced from the same - * `CODING_AGENT_DRIVER_CONFIG_ENV` map the real driver factory reads -- never a hand-duplicated var-name list - * that could drift. Empty input skips that one var; its built-in default (if any) applies at run time as usual. - */ -export declare function promptCompanionVars(io: WizardIo, provider: string): Promise>; -/** - * Run the interactive onboarding wizard end to end: collect GITHUB_TOKEN (pasted, or via device-flow - * authorization when configured -- see collectGithubToken) + optional provider config, write the starter .env, - * initialize laptop state, then rerun the existing offline doctor checks against the collected values. Returns - * doctor's exit code. `io` is injected so tests never touch a real terminal; `options.fetchImpl`/`sleepFn` are - * injected so tests never make a real network call or wait on a real timer during device-flow polling. - */ -export declare function runInteractiveInit(env: Record, cwd: string, io: WizardIo, options?: RunInteractiveInitOptions): Promise; -/** - * Real terminal I/O for the wizard. Masked input is implemented by overriding readline's own output-write hook - * to render `*` instead of the typed prompt's characters while the interface is still doing its normal - * cooked-mode line editing (Enter/Backspace all still work exactly as with a plain prompt) -- no raw-mode byte - * handling and no extra dependency. `input`/`output` are parameters (defaulting to the real stdio) purely so - * tests can drive the exact same code path with fake streams instead of a real terminal. - */ -export declare function createWizardIo(input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): WizardIo & { - close: () => void; - isInteractive: boolean; -}; diff --git a/packages/loopover-miner/lib/init-wizard.js b/packages/loopover-miner/lib/init-wizard.js deleted file mode 100644 index da6c4ff2ae..0000000000 --- a/packages/loopover-miner/lib/init-wizard.js +++ /dev/null @@ -1,207 +0,0 @@ -import { createInterface } from "node:readline"; -import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { CODING_AGENT_DRIVER_CONFIG_ENV, CODING_AGENT_DRIVER_NAMES } from "@loopover/engine"; -import { initLaptopState } from "./laptop-init.js"; -import { resolveMinerStateDir, runDoctor } from "./status.js"; -import { DeviceFlowError, resolveAmsOauthClientId, runDeviceFlowAuthorization } from "./oauth-device-flow.js"; -const COMPANION_VAR_LABELS = { model: "model override", timeoutMs: "timeout in milliseconds" }; -/** Where the wizard writes its starter .env file: the miner state dir, the same directory `init` already uses - * for laptop-state.sqlite3. */ -export function resolveWizardEnvFilePath(env = process.env) { - return join(resolveMinerStateDir(env), ".env"); -} -/** Render collected `[KEY, value]` pairs as sourceable `KEY=value` lines, one per entry, insertion order. Pure - * and filesystem-free so it is directly testable. */ -export function renderWizardEnvFile(entries) { - if (entries.length === 0) - return ""; - return `${entries.map(([key, value]) => `${key}=${value}`).join("\n")}\n`; -} -async function promptRequiredMasked(io, question) { - for (;;) { - const answer = (await io.promptMasked(question)).trim(); - if (answer) - return answer; - io.writeLine("A value is required -- please try again."); - } -} -async function promptAuthMethod(io) { - io.writeLine("How would you like to authorize loopover-miner?"); - io.writeLine(" 1) Authorize with GitHub (recommended -- no token to copy)"); - io.writeLine(" 2) Paste a GitHub token (personal access token)"); - for (;;) { - const answer = (await io.promptText("Choice [1/2, default 1]: ")).trim(); - if (!answer || answer === "1") - return "device"; - if (answer === "2") - return "token"; - io.writeLine("Enter 1 or 2."); - } -} -/** - * Collect a GitHub credential for the wizard's starter .env. When LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID isn't - * configured (today's default, before the loopover-ams App is registered), this is IDENTICAL to the original - * masked-token-only prompt -- no menu, no behavior change. Once configured, offers device-flow authorization as - * the default choice, with the original pasted-token path still available (option 2) and as the automatic - * fallback on any device-flow failure -- never a hard dependency. - */ -async function collectGithubToken(io, env, options) { - const clientId = resolveAmsOauthClientId(env); - if (!clientId) - return promptRequiredMasked(io, "GitHub token (input hidden): "); - const method = await promptAuthMethod(io); - if (method === "token") - return promptRequiredMasked(io, "GitHub token (input hidden): "); - try { - const { accessToken } = await runDeviceFlowAuthorization({ - clientId, - fetchFn: options.fetchImpl, - sleepFn: options.sleepFn, - onCode: (code) => { - io.writeLine(""); - io.writeLine(`To authorize, visit ${code.verificationUri} and enter code: ${code.userCode}`); - io.writeLine("Waiting for authorization..."); - }, - }); - io.writeLine("Authorized."); - return accessToken; - } - catch (error) { - const reason = error instanceof DeviceFlowError ? error.code : "device_flow_failed"; - io.writeLine(`Device-flow authorization failed (${reason}) -- falling back to a pasted token.`); - return promptRequiredMasked(io, "GitHub token (input hidden): "); - } -} -/** - * Menu selection sourced from the engine's own `CODING_AGENT_DRIVER_NAMES`, so the choices can never drift from - * what the driver factory actually resolves. Empty input SKIPS provider selection entirely (leaves - * MINER_CODING_AGENT_PROVIDER unwritten, deferring to whatever default the CLI already resolves) -- distinct - * from explicitly choosing the `noop` entry. - */ -export async function promptProviderSelection(io) { - io.writeLine("Select a coding-agent provider (press Enter to skip and use the default):"); - CODING_AGENT_DRIVER_NAMES.forEach((name, index) => { - io.writeLine(` ${index + 1}) ${name}`); - }); - for (;;) { - const answer = (await io.promptText(`Provider [1-${CODING_AGENT_DRIVER_NAMES.length}, or Enter to skip]: `)).trim(); - if (!answer) - return null; - const index = Number(answer) - 1; - if (Number.isInteger(index) && index >= 0 && index < CODING_AGENT_DRIVER_NAMES.length) { - return CODING_AGENT_DRIVER_NAMES[index]; - } - io.writeLine(`Enter a number from 1 to ${CODING_AGENT_DRIVER_NAMES.length}, or press Enter to skip.`); - } -} -/** - * Optional, skippable per-provider companion vars (model override / timeout), sourced from the same - * `CODING_AGENT_DRIVER_CONFIG_ENV` map the real driver factory reads -- never a hand-duplicated var-name list - * that could drift. Empty input skips that one var; its built-in default (if any) applies at run time as usual. - */ -export async function promptCompanionVars(io, provider) { - const varsForProvider = CODING_AGENT_DRIVER_CONFIG_ENV[provider] ?? {}; - const collected = []; - for (const [kind, envVarName] of Object.entries(varsForProvider)) { - const label = COMPANION_VAR_LABELS[kind]; - const answer = (await io.promptText(`Optional ${label} for ${provider} (env ${envVarName}) [Enter to skip]: `)).trim(); - if (answer) - collected.push([envVarName, answer]); - } - return collected; -} -/** - * Run the interactive onboarding wizard end to end: collect GITHUB_TOKEN (pasted, or via device-flow - * authorization when configured -- see collectGithubToken) + optional provider config, write the starter .env, - * initialize laptop state, then rerun the existing offline doctor checks against the collected values. Returns - * doctor's exit code. `io` is injected so tests never touch a real terminal; `options.fetchImpl`/`sleepFn` are - * injected so tests never make a real network call or wait on a real timer during device-flow polling. - */ -export async function runInteractiveInit(env, cwd, io, options = {}) { - // #6846: fail fast, not silently forever. `io.isInteractive` is only ever `false` for a real - // `createWizardIo()` adapter over a non-TTY stdin (a test's fake `io` has no such field and stays - // interactive by default, so every existing test is unaffected) -- an operator running this over a - // no-pty SSH session or a CI/fleet script gets clear, actionable guidance instead of a hang on the - // wizard's first prompt, which can never receive a real line of input. - if (io.isInteractive === false) { - io.writeLine("init --interactive requires a real terminal (no TTY detected on stdin)."); - io.writeLine("For an unattended/fleet setup, skip this wizard and set these env vars directly instead:"); - io.writeLine(" - GITHUB_TOKEN (your GitHub credential)"); - io.writeLine(" - MINER_CODING_AGENT_PROVIDER (claude-cli or codex-cli)"); - io.writeLine(" - ANTHROPIC_API_KEY for claude-cli, or OPENAI_API_KEY for codex-cli"); - io.writeLine("Then verify with: loopover-miner doctor"); - return 3; - } - const githubToken = await collectGithubToken(io, env, options); - const provider = await promptProviderSelection(io); - const entries = [["GITHUB_TOKEN", githubToken]]; - if (provider) { - entries.push(["MINER_CODING_AGENT_PROVIDER", provider]); - entries.push(...(await promptCompanionVars(io, provider))); - } - const stateDir = resolveMinerStateDir(env); - mkdirSync(stateDir, { recursive: true, mode: 0o700 }); - const envFilePath = resolveWizardEnvFilePath(env); - // { mode: 0o600 } on writeFileSync applies only when the file is newly created -- an existing file (e.g. from - // a prior wizard run, or hand-created by the operator with looser permissions) keeps its current mode across - // a write. The chmodSync below still runs unconditionally so the end state is always 0600 either way; the - // writeFileSync mode option exists so a BRAND NEW file is never briefly readable at the default umask - // permissions between being created and being locked down. - writeFileSync(envFilePath, renderWizardEnvFile(entries), { mode: 0o600 }); - chmodSync(envFilePath, 0o600); - io.writeLine(`wrote ${envFilePath}`); - const initResult = initLaptopState(env); - io.writeLine(`initialized ${initResult.stateDir}`); - io.writeLine(`sqlite: ${initResult.dbPath}${initResult.created ? "" : " (already existed)"}`); - const mergedEnv = { ...env }; - for (const [key, value] of entries) - mergedEnv[key] = value; - io.writeLine(""); - io.writeLine("Running doctor against the new configuration:"); - return runDoctor([], mergedEnv, cwd); -} -/** - * Real terminal I/O for the wizard. Masked input is implemented by overriding readline's own output-write hook - * to render `*` instead of the typed prompt's characters while the interface is still doing its normal - * cooked-mode line editing (Enter/Backspace all still work exactly as with a plain prompt) -- no raw-mode byte - * handling and no extra dependency. `input`/`output` are parameters (defaulting to the real stdio) purely so - * tests can drive the exact same code path with fake streams instead of a real terminal. - */ -export function createWizardIo(input = process.stdin, output = process.stdout) { - const rl = createInterface({ input, output, terminal: true }); - // `_writeToOutput` is readline's own undocumented internal hook (not part of Node's public Interface type), - // the standard technique for masking input while keeping normal cooked-mode line editing. - const rlInternal = rl; - const originalWriteToOutput = rlInternal._writeToOutput.bind(rl); - let masking = false; - rlInternal._writeToOutput = (stringToWrite) => { - originalWriteToOutput(masking ? "*" : stringToWrite); - }; - return { - // #6846: whether `input` is a real, interactive terminal -- `runInteractiveInit` checks this BEFORE - // issuing its first prompt, so a no-TTY invocation (piped stdin, a plain `ssh host "loopover-miner init - // --interactive"` with no allocated pty) fails fast with actionable guidance instead of hanging forever - // on a `readline` prompt that can never receive a real line of input. - isInteractive: Boolean(input.isTTY), - promptText(question) { - return new Promise((resolve) => rl.question(question, resolve)); - }, - promptMasked(question) { - return new Promise((resolve) => { - rl.question(question, (answer) => { - masking = false; - resolve(answer); - }); - masking = true; - }); - }, - writeLine(text) { - output.write(`${text}\n`); - }, - close() { - rl.close(); - }, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5pdC13aXphcmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbml0LXdpemFyZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQ2hELE9BQU8sRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLGFBQWEsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5RCxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSw4QkFBOEIsRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzdGLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNuRCxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsU0FBUyxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzlELE9BQU8sRUFBRSxlQUFlLEVBQUUsdUJBQXVCLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQWdDOUcsTUFBTSxvQkFBb0IsR0FBMkIsRUFBRSxLQUFLLEVBQUUsZ0JBQWdCLEVBQUUsU0FBUyxFQUFFLHlCQUF5QixFQUFFLENBQUM7QUFFdkg7Z0NBQ2dDO0FBQ2hDLE1BQU0sVUFBVSx3QkFBd0IsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUM1RixPQUFPLElBQUksQ0FBQyxvQkFBb0IsQ0FBQyxHQUFHLENBQUMsRUFBRSxNQUFNLENBQUMsQ0FBQztBQUNqRCxDQUFDO0FBRUQ7c0RBQ3NEO0FBQ3RELE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxPQUFpRDtJQUNuRixJQUFJLE9BQU8sQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQ3BDLE9BQU8sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLEVBQUUsRUFBRSxDQUFDLEdBQUcsR0FBRyxJQUFJLEtBQUssRUFBRSxDQUFDLENBQUMs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/laptop-init.d.ts b/packages/loopover-miner/lib/laptop-init.d.ts deleted file mode 100644 index a8ebb6e431..0000000000 --- a/packages/loopover-miner/lib/laptop-init.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -export type LaptopInitResult = { - stateDir: string; - dbPath: string; - created: boolean; -}; -export type DoctorCheck = { - name: string; - ok: boolean; - detail: string; -}; -export type GithubTokenVerification = { - ok: boolean; - login: string | null; - scopes: string[]; - detail: string; -}; -/** Path to the laptop-mode SQLite bootstrap file inside the miner state directory. */ -export declare function resolveLaptopStateDbPath(env?: Record): string; -/** Create the state dir and SQLite file. Re-running is idempotent and never clobbers existing rows. */ -export declare function initLaptopState(env?: Record): LaptopInitResult; -export declare function checkLaptopStateSqlite(env?: Record): DoctorCheck; -/** Exported so callers that only need a presence boolean (e.g. status.js's `driver` section, #5164) can reuse - * this PATH scan directly instead of duplicating it or parsing a DoctorCheck's detail string. */ -export declare function findExecutableOnPath(name: string, env?: Record): string | null; -/** Informational only — Docker is never required for laptop mode. */ -export declare function checkDockerPresent(options?: { - env?: Record; - resolveDockerPath?: () => string | null; -}): DoctorCheck; -export declare function resolveCodexAuthPath(env?: Record): string; -/** - * Validate a GitHub token with one authenticated API call. - * - * The classic OAuth scope header is advisory when GitHub reports it: if GitHub returns `repo` or - * `public_repo`, we treat the token as sufficiently scoped for miner setup. If GitHub omits the classic - * scope header altogether, the token is still considered valid and the response is reported as "scopes not - * reported" — that keeps fine-grained tokens usable while still surfacing the scopes GitHub did return. - */ -export declare function verifyGithubToken(options?: { - githubToken?: string; - fetchImpl?: typeof fetch; - apiBaseUrl?: string; - timeoutMs?: number; -}): Promise; -/** Informational unless `MINER_CODING_AGENT_PROVIDER=claude-cli` (#5165), in which case a missing CLI fails - * doctor. The auth probe is read-only and never spawns the CLI: it surfaces, proactively, the SAME condition - * claude checks at call time — `CLAUDE_CODE_OAUTH_TOKEN` present (see createClaudeCodeAi, src/selfhost/ai.ts). */ -export declare function checkClaudeCliPresent(options?: { - env?: Record; - resolveClaudePath?: () => string | null; -}): DoctorCheck; -/** Informational unless `MINER_CODING_AGENT_PROVIDER=codex-cli` (#5165), in which case a missing CLI fails - * doctor — mirrors {@link checkClaudeCliPresent}. The auth probe checks the same read-only condition - * assertCodexAuthConfigured uses at call time: codex's `auth.json` is readable. */ -export declare function checkCodexCliPresent(options?: { - env?: Record; - resolveCodexPath?: () => string | null; - resolveCodexAuthPath?: () => string; -}): DoctorCheck; -export declare function runInit(args?: string[], env?: Record): Promise; diff --git a/packages/loopover-miner/lib/laptop-init.js b/packages/loopover-miner/lib/laptop-init.js deleted file mode 100644 index f5f1c1f7b2..0000000000 --- a/packages/loopover-miner/lib/laptop-init.js +++ /dev/null @@ -1,323 +0,0 @@ -import { accessSync, chmodSync, constants, existsSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { delimiter, join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import { applySchemaMigrations } from "./schema-version.js"; -import { reportCliFailure } from "./cli-error.js"; -import { resolveGitHubToken } from "./github-token-resolution.js"; -const githubApiBaseUrl = "https://api.github.com"; -const githubApiVersion = "2022-11-28"; -const classicRepoScopes = new Set(["repo", "public_repo"]); -const defaultDbFileName = "laptop-state.sqlite3"; -/** Local state directory (mirrors `resolveMinerStateDir` in status.js — kept local to avoid import cycles). */ -function resolveMinerStateDir(env = process.env) { - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" - ? env.LOOPOVER_MINER_CONFIG_DIR.trim() - : ""; - if (explicitConfigDir) - return explicitConfigDir; - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner"); -} -/** Path to the laptop-mode SQLite bootstrap file inside the miner state directory. */ -export function resolveLaptopStateDbPath(env = process.env) { - return join(resolveMinerStateDir(env), defaultDbFileName); -} -/** Create the state dir and SQLite file. Re-running is idempotent and never clobbers existing rows. */ -export function initLaptopState(env = process.env) { - const stateDir = resolveMinerStateDir(env); - const dbPath = resolveLaptopStateDbPath(env); - mkdirSync(stateDir, { recursive: true, mode: 0o700 }); - const created = !existsSync(dbPath); - const db = new DatabaseSync(dbPath); - db.exec(` - CREATE TABLE IF NOT EXISTS laptop_meta ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations (none yet). - applySchemaMigrations(db, []); - if (created) { - db.prepare("INSERT INTO laptop_meta (key, value) VALUES ('initialized_at', ?)") - .run(new Date().toISOString()); - } - chmodSync(dbPath, 0o600); - db.close(); - return { stateDir, dbPath, created }; -} -export function checkLaptopStateSqlite(env = process.env) { - const dbPath = resolveLaptopStateDbPath(env); - if (!existsSync(dbPath)) { - return { - name: "laptop-state-sqlite", - ok: false, - detail: `${dbPath}: not found (run loopover-miner init)`, - }; - } - try { - // `readOnly` (camelCase) -- node:sqlite silently IGNORES `readonly` (lowercase) as an unrecognized option - // and opens read-write anyway, which would break doctor's own "no writes, no network" contract. Same - // footgun already documented in claim-ledger.js's openClaimLedgerReadOnly and purge-cli.js. - const db = new DatabaseSync(dbPath, { readOnly: true }); - db.prepare("SELECT 1").get(); - db.close(); - return { name: "laptop-state-sqlite", ok: true, detail: dbPath }; - } - catch (error) { - // Defensive: node:sqlite's DatabaseSync/prepare/get always throw real Error instances for every failure - // mode reachable through this real (non-mocked) file path -- deliberately not exercised by a contrived - // unit test, since forcing a non-Error throw here would require mocking node:sqlite itself. - return { - name: "laptop-state-sqlite", - ok: false, - detail: `${dbPath}: ${error instanceof Error ? error.message : "not readable"}`, - }; - } -} -/** Exported so callers that only need a presence boolean (e.g. status.js's `driver` section, #5164) can reuse - * this PATH scan directly instead of duplicating it or parsing a DoctorCheck's detail string. */ -export function findExecutableOnPath(name, env = process.env) { - const pathValue = typeof env.PATH === "string" ? env.PATH : ""; - for (const pathEntry of pathValue.split(delimiter)) { - if (!pathEntry) - continue; - const candidate = join(pathEntry, name); - try { - accessSync(candidate, constants.X_OK); - return candidate; - } - catch { - // Keep scanning: PATH often contains missing or unreadable entries. - } - } - return null; -} -/** Informational only — Docker is never required for laptop mode. */ -export function checkDockerPresent(options = {}) { - const resolveDockerPath = options.resolveDockerPath - ?? (() => findExecutableOnPath("docker", options.env)); - const dockerPath = resolveDockerPath(); - return { - name: "docker-present", - ok: true, - detail: dockerPath ? `found at ${dockerPath}` : "not installed (optional for laptop mode)", - }; -} -// Codex stores credentials at `$CODEX_HOME/auth.json`, else `$HOME/.codex/auth.json` — mirrors -// resolveCodexAuthPath in src/selfhost/ai.ts, kept local so the offline miner package never imports the -// Worker AI module. Exported so `doctor`'s provider-credential check (status.js, #5170) resolves the SAME -// path this file's own codex auth probe uses, instead of duplicating the location logic. -export function resolveCodexAuthPath(env = process.env) { - const base = env.CODEX_HOME ?? join(env.HOME ?? homedir(), ".codex"); - return join(base, "auth.json"); -} -// `githubToken` is always a real (already-trimmed) string here: this function is private and its sole caller, -// verifyGithubToken, already coerces `options.githubToken` to a trimmed string before passing it in. -function githubHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "user-agent": "loopover-miner", - "x-github-api-version": githubApiVersion, - }; - if (githubToken) - headers.authorization = `Bearer ${githubToken}`; - return headers; -} -function parseScopesHeader(scopesHeader) { - return typeof scopesHeader === "string" && scopesHeader.trim() - ? scopesHeader.split(",").map((scope) => scope.trim()).filter(Boolean) - : []; -} -// `scopes` is always non-empty here: both call sites already guard `scopes.length > 0` before calling this. -function formatScopes(scopes) { - return scopes.join(", "); -} -function hasRepoAccessScope(scopes) { - return scopes.some((scope) => classicRepoScopes.has(scope)); -} -function readGithubErrorMessage(payload, status) { - const record = payload; - if (record && typeof record === "object" && typeof record.message === "string" && record.message.trim()) { - return record.message.trim(); - } - return `GitHub returned HTTP ${status}`; -} -/** - * Validate a GitHub token with one authenticated API call. - * - * The classic OAuth scope header is advisory when GitHub reports it: if GitHub returns `repo` or - * `public_repo`, we treat the token as sufficiently scoped for miner setup. If GitHub omits the classic - * scope header altogether, the token is still considered valid and the response is reported as "scopes not - * reported" — that keeps fine-grained tokens usable while still surfacing the scopes GitHub did return. - */ -export async function verifyGithubToken(options = {}) { - const githubToken = typeof options.githubToken === "string" ? options.githubToken.trim() : ""; - const fetchImpl = options.fetchImpl ?? fetch; - const apiBaseUrl = typeof options.apiBaseUrl === "string" && options.apiBaseUrl.trim() - ? options.apiBaseUrl.trim().replace(/\/+$/, "") || githubApiBaseUrl - : githubApiBaseUrl; - const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 5000; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - let response; - try { - response = await fetchImpl(`${apiBaseUrl}/user`, { - method: "GET", - headers: githubHeaders(githubToken), - signal: controller.signal, - }); - } - catch (error) { - const detail = controller.signal.aborted - ? `timed out after ${timeoutMs}ms` - : error instanceof Error - ? error.message - : "request failed"; - return { - ok: false, - login: null, - scopes: [], - detail: `GITHUB_TOKEN verification failed: ${detail}`, - }; - } - finally { - clearTimeout(timeout); - } - const payload = await response.json().catch(() => null); - const scopesHeader = response.headers.get("x-oauth-scopes"); - const scopesHeaderPresent = response.headers.has("x-oauth-scopes"); - const scopes = parseScopesHeader(scopesHeader); - const payloadRecord = payload; - const login = payloadRecord && typeof payloadRecord === "object" && typeof payloadRecord.login === "string" ? payloadRecord.login.trim() : ""; - if (!response.ok) { - return { - ok: false, - login: null, - scopes, - detail: `GITHUB_TOKEN verification failed: ${readGithubErrorMessage(payload, response.status)}`, - }; - } - if (scopesHeaderPresent && scopes.length === 0) { - return { - ok: false, - login: login || null, - scopes, - detail: "GITHUB_TOKEN is valid, but GitHub returned an empty x-oauth-scopes header; reissue it with repo access for miner setup.", - }; - } - if (scopes.length > 0 && !hasRepoAccessScope(scopes)) { - return { - ok: false, - login: login || null, - scopes, - detail: `GITHUB_TOKEN is valid, but GitHub reported only ${formatScopes(scopes)}; reissue it with repo access for miner setup.`, - }; - } - return { - ok: true, - login: login || null, - scopes, - detail: scopes.length > 0 - ? `validated GitHub token for ${login || "unknown user"}; scopes: ${formatScopes(scopes)}` - : `validated GitHub token for ${login || "unknown user"}; GitHub did not report classic OAuth scopes`, - }; -} -/** A coding-agent CLI is only needed once a driver provider is configured (#4289) — gated by - * `MINER_CODING_AGENT_PROVIDER` (#5165). When that provider is NOT the CLI being checked, absence is - * advisory (`ok: true`), mirroring checkDockerPresent's optional tone. When it IS configured and the CLI is - * missing, `ok: false` — every attempt will fail without it. The auth probe (once found) stays advisory - * either way, since an unauthenticated-but-installed CLI is a separate, already-visible warning. */ -function codingAgentProviderConfiguredFor(env, providerName) { - return env.MINER_CODING_AGENT_PROVIDER === providerName; -} -/** Informational unless `MINER_CODING_AGENT_PROVIDER=claude-cli` (#5165), in which case a missing CLI fails - * doctor. The auth probe is read-only and never spawns the CLI: it surfaces, proactively, the SAME condition - * claude checks at call time — `CLAUDE_CODE_OAUTH_TOKEN` present (see createClaudeCodeAi, src/selfhost/ai.ts). */ -export function checkClaudeCliPresent(options = {}) { - const env = options.env ?? process.env; - const claudePath = (options.resolveClaudePath ?? (() => findExecutableOnPath("claude", env)))(); - if (!claudePath) { - const configured = codingAgentProviderConfiguredFor(env, "claude-cli"); - return { - name: "claude-cli-present", - ok: !configured, - detail: configured - ? "not installed — MINER_CODING_AGENT_PROVIDER is set to claude-cli, every attempt will fail without it" - : "not installed (optional until a coding-agent driver is configured)", - }; - } - const authed = typeof env.CLAUDE_CODE_OAUTH_TOKEN === "string" && env.CLAUDE_CODE_OAUTH_TOKEN.length > 0; - return { - name: "claude-cli-present", - ok: true, - detail: authed ? `found at ${claudePath} (authenticated)` : `found at ${claudePath} (not authenticated: set CLAUDE_CODE_OAUTH_TOKEN)`, - }; -} -/** Informational unless `MINER_CODING_AGENT_PROVIDER=codex-cli` (#5165), in which case a missing CLI fails - * doctor — mirrors {@link checkClaudeCliPresent}. The auth probe checks the same read-only condition - * assertCodexAuthConfigured uses at call time: codex's `auth.json` is readable. */ -export function checkCodexCliPresent(options = {}) { - const env = options.env ?? process.env; - const codexPath = (options.resolveCodexPath ?? (() => findExecutableOnPath("codex", env)))(); - if (!codexPath) { - const configured = codingAgentProviderConfiguredFor(env, "codex-cli"); - return { - name: "codex-cli-present", - ok: !configured, - detail: configured - ? "not installed — MINER_CODING_AGENT_PROVIDER is set to codex-cli, every attempt will fail without it" - : "not installed (optional until a coding-agent driver is configured)", - }; - } - const authPath = (options.resolveCodexAuthPath ?? (() => resolveCodexAuthPath(env)))(); - let authed = false; - try { - accessSync(authPath, constants.R_OK); - authed = true; - } - catch { - // auth.json missing or unreadable — codex would fail for lack of credentials at call time. - } - if (authed) { - return { name: "codex-cli-present", ok: true, detail: `found at ${codexPath} (authenticated)` }; - } - // codex-cli IS the configured driver but auth.json is missing/expired: a more specific, actionable remediation - // than the generic advisory below, mirroring ORB's codexAuthReadinessProbe/assertCodexAuthConfigured wording - // (#5166). `ok` stays true either way (unchanged by this issue, see #5165) since the CLI itself IS present -- - // only the CLI-absent case is a hard doctor failure. - const detail = codingAgentProviderConfiguredFor(env, "codex-cli") - ? `found at ${codexPath} but auth.json is missing or expired — run \`codex auth\` to authenticate before attempts run` - : `found at ${codexPath} (not authenticated: run \`codex auth\`)`; - return { name: "codex-cli-present", ok: true, detail }; -} -export async function runInit(args = [], env = process.env) { - const verifyToken = args.includes("--verify-token"); - const jsonOutput = args.includes("--json"); - let verification = null; - if (verifyToken) { - // resolveGitHubToken types `env` as the ambient (Cloudflare-Workers-augmented) `NodeJS.ProcessEnv`, - // stricter than this file's own `Record` -- this package runs under plain - // Node, and any object matching this shape genuinely satisfies resolveGitHubToken at runtime either way. - const resolveToken = resolveGitHubToken; - verification = await verifyGithubToken({ githubToken: (await resolveToken(env)) ?? "" }); - if (!verification.ok) { - return reportCliFailure(jsonOutput, verification.detail, 1); - } - } - const result = initLaptopState(env); - if (jsonOutput) { - console.log(JSON.stringify(verification ? { ...result, tokenVerification: verification } : result, null, 2)); - } - else { - console.log(`initialized ${result.stateDir}`); - console.log(`sqlite: ${result.dbPath}${result.created ? "" : " (already existed)"}`); - if (verification) { - console.log(`token: ${verification.detail}`); - } - } - return 0; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGFwdG9wLWluaXQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJsYXB0b3AtaW5pdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxTQUFTLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNsRixPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQzVDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDNUQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDbEQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFFbEUsTUFBTSxnQkFBZ0IsR0FBRyx3QkFBd0IsQ0FBQztBQUNsRCxNQUFNLGdCQUFnQixHQUFHLFlBQVksQ0FBQztBQUN0QyxNQUFNLGlCQUFpQixHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLGFBQWEsQ0FBQyxDQUFDLENBQUM7QUFDM0QsTUFBTSxpQkFBaUIsR0FBRyxzQkFBc0IsQ0FBQztBQXFCakQsK0dBQStHO0FBQy9HLFNBQVMsb0JBQW9CLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDakYsTUFBTSxpQkFBaUIsR0FBRyxPQUFPLEdBQUcsQ0FBQyx5QkFBeUIsS0FBSyxRQUFRO1FBQ3pFLENBQUMsQ0FBQyxHQUFHLENBQUMseUJBQXlCLENBQUMsSUFBSSxFQUFFO1FBQ3RDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDUCxJQUFJLGlCQUFpQjtRQUFFLE9BQU8saUJBQWlCLENBQUM7SUFFaEQsTUFBTSxVQUFVLEdBQUcsT0FBTyxHQUFHLENBQUMsZUFBZSxLQUFL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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/live-issue-snapshot.d.ts b/packages/loopover-miner/lib/live-issue-snapshot.d.ts deleted file mode 100644 index f6ba293b4f..0000000000 --- a/packages/loopover-miner/lib/live-issue-snapshot.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { LiveIssueSnapshot } from "./submission-freshness-check.js"; -export type LiveIssueSnapshotFetch = (url: string, init: { - method: string; - headers: Record; - body: string; -}) => Promise<{ - ok: boolean; - status: number; - json: () => Promise; -}>; -type LiveIssueSnapshotOptions = { - githubToken?: string; - graphqlUrl?: string; - fetchImpl?: LiveIssueSnapshotFetch; - requestTimeoutMs?: number; -}; -/** - * Real fetchLiveIssueSnapshot implementation: the live-state answer AttemptDeps/SubmissionFreshnessDeps - * need, built from a single GraphQL round-trip. Returns null on any malformed input, transport failure, or - * unrecognized GitHub response -- callers already treat a null snapshot as "state unavailable", so this - * never throws. - */ -export declare function fetchLiveIssueSnapshot(repoFullName: string, issueNumber: number, options?: LiveIssueSnapshotOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/live-issue-snapshot.js b/packages/loopover-miner/lib/live-issue-snapshot.js deleted file mode 100644 index bf2c5c9c5b..0000000000 --- a/packages/loopover-miner/lib/live-issue-snapshot.js +++ /dev/null @@ -1,124 +0,0 @@ -// Real GitHub-backed fetchLiveIssueSnapshot (#5132, Wave 3.5). AttemptDeps.fetchLiveIssueSnapshot and -// SubmissionFreshnessDeps.fetchLiveIssueSnapshot (submission-freshness-check.js) share this one shape: -// "is this issue still open, and is it already addressed by another PR" -- the live-state answer -// checkSubmissionFreshness needs before every submission. Uses GitHub's GraphQL -// `closedByPullRequestsReferences` connection rather than a body-text/search-API heuristic: it's GitHub's -// own authoritative, closing-keyword-aware answer to "which PRs will close this issue" -- the same signal -// the platform itself uses to auto-close on merge, not a regex we'd have to keep in sync with GitHub's own -// closing-keyword parsing. -const DEFAULT_GRAPHQL_URL = "https://api.github.com/graphql"; -const GITHUB_API_VERSION = "2022-11-28"; -const MAX_REFERENCING_PRS = 50; -const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; -const LIVE_ISSUE_SNAPSHOT_QUERY = ` - query($owner: String!, $repo: String!, $number: Int!, $maxPrs: Int!) { - repository(owner: $owner, name: $repo) { - issue(number: $number) { - state - closedByPullRequestsReferences(first: $maxPrs) { - nodes { - number - state - author { login } - createdAt - } - } - } - } - } -`; -function githubGraphqlHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "content-type": "application/json", - "user-agent": "loopover-miner", - "x-github-api-version": GITHUB_API_VERSION, - }; - const token = typeof githubToken === "string" ? githubToken.trim() : ""; - if (token) - headers.authorization = `Bearer ${token}`; - return headers; -} -function normalizeIssueOrPrState(rawState) { - return typeof rawState === "string" ? rawState.toLowerCase() : ""; -} -function normalizeReferencingPr(node) { - if (!node || typeof node !== "object") - return null; - const record = node; - if (!Number.isInteger(record.number) || record.number <= 0) - return null; - const state = normalizeIssueOrPrState(record.state); - if (state !== "open" && state !== "closed" && state !== "merged") - return null; - const authorLogin = typeof record.author?.login === "string" ? record.author.login : ""; - // GitHub's real PR creation timestamp (ISO 8601), when present -- null otherwise (never fabricated). Not - // an ordering signal for the maintainer gate's own duplicate-cluster election (duplicate-winner.ts's own - // doc explains why: a PR can be backdated by editing an old placeholder to add the linked issue later), but - // it's the only real, publicly-observable claim-time proxy claim-conflict-resolver.js's own client-side - // caller has for a THIRD-PARTY PR -- unlike loopover's own server, the miner has no continuous observation - // history to derive a true "first linked" timestamp from. - const createdAt = typeof record.createdAt === "string" ? record.createdAt : null; - return { number: record.number, state, authorLogin, createdAt }; -} -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - return null; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - return { owner, repo }; -} -/** - * Real fetchLiveIssueSnapshot implementation: the live-state answer AttemptDeps/SubmissionFreshnessDeps - * need, built from a single GraphQL round-trip. Returns null on any malformed input, transport failure, or - * unrecognized GitHub response -- callers already treat a null snapshot as "state unavailable", so this - * never throws. - */ -export async function fetchLiveIssueSnapshot(repoFullName, issueNumber, options = {}) { - const target = parseRepoFullName(repoFullName); - if (!target || !Number.isInteger(issueNumber) || issueNumber <= 0) - return null; - const graphqlUrl = typeof options.graphqlUrl === "string" && options.graphqlUrl.trim() ? options.graphqlUrl.trim() : DEFAULT_GRAPHQL_URL; - const githubToken = options.githubToken ?? process.env.GITHUB_TOKEN ?? ""; - // Cast: ambient `fetch` is CF-Workers-flavored; the public inject seam is the narrower LiveIssueSnapshotFetch. - const fetchImpl = (options.fetchImpl ?? fetch); - const requestTimeoutMs = Number.isInteger(options.requestTimeoutMs) && options.requestTimeoutMs > 0 - ? options.requestTimeoutMs - : DEFAULT_REQUEST_TIMEOUT_MS; - // Bounded so a stalled connection can't hang this "never throws" fetcher forever (#miner-github-read-timeouts): - // a timeout falls into the SAME catch as any other transport failure, which the caller (checkSubmissionFreshness) - // already treats as "live_state_unavailable" -- a fail-closed abort distinct from "issue_closed"/"already_addressed", - // never confused with a confirmed-gone issue. - let response; - try { - // Cast: runtime always passes `signal`; the public LiveIssueSnapshotFetch init omits it (mock-friendly). - response = await fetchImpl(graphqlUrl, { - method: "POST", - headers: githubGraphqlHeaders(githubToken), - body: JSON.stringify({ - query: LIVE_ISSUE_SNAPSHOT_QUERY, - variables: { owner: target.owner, repo: target.repo, number: issueNumber, maxPrs: MAX_REFERENCING_PRS }, - }), - signal: AbortSignal.timeout(requestTimeoutMs), - }); - } - catch { - return null; - } - if (!response.ok) - return null; - const payload = await response.json().catch(() => null); - if (!payload || typeof payload !== "object" || payload.errors) - return null; - const issue = payload.data?.repository?.issue; - const state = normalizeIssueOrPrState(issue?.state); - if (state !== "open" && state !== "closed") - return null; - const nodes = Array.isArray(issue?.closedByPullRequestsReferences?.nodes) - ? issue.closedByPullRequestsReferences.nodes - : []; - const referencingPrs = nodes.map(normalizeReferencingPr).filter((pr) => pr !== null); - return { state, referencingPrs }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGl2ZS1pc3N1ZS1zbmFwc2hvdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImxpdmUtaXNzdWUtc25hcHNob3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsc0dBQXNHO0FBQ3RHLHVHQUF1RztBQUN2RyxpR0FBaUc7QUFDakcsZ0ZBQWdGO0FBQ2hGLDBHQUEwRztBQUMxRywwR0FBMEc7QUFDMUcsMkdBQTJHO0FBQzNHLDJCQUEyQjtBQUkzQixNQUFNLG1CQUFtQixHQUFHLGdDQUFnQyxDQUFDO0FBQzdELE1BQU0sa0JBQWtCLEdBQUcsWUFBWSxDQUFDO0FBQ3hDLE1BQU0sbUJBQW1CLEdBQUcsRUFBRSxDQUFDO0FBQy9CLE1BQU0sMEJBQTBCLEdBQUcsTUFBTSxDQUFDO0FBRTFDLE1BQU0seUJBQXlCLEdBQUc7Ozs7Ozs7Ozs7Ozs7Ozs7Q0FnQmpDLENBQUM7QUFvQkYsU0FBUyxvQkFBb0IsQ0FBQyxXQUFvQjtJQUNoRCxNQUFNLE9BQU8sR0FBMkI7UUFDdEMsTUFBTSxFQUFFLDZCQUE2QjtRQUNyQyxjQUFjLEVBQUUsa0JBQWtCO1FBQ2xDLFlBQVksRUFBRSxnQkFBZ0I7UUFDOUIsc0JBQXNCLEVBQUUsa0JBQWtCO0tBQzNDLENBQUM7SUFDRixNQUFNLEtBQUssR0FBRyxPQUFPLFdBQVcsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3hFLElBQUksS0FBSztRQUFFLE9BQU8sQ0FBQyxhQUFhLEdBQUcsVUFBVSxLQUFLLEVBQUUsQ0FBQztJQUNyRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQsU0FBUyx1QkFBdUIsQ0FBQyxRQUFpQjtJQUNoRCxPQUFPLE9BQU8sUUFBUSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7QUFDcEUsQ0FBQztBQUVELFNBQVMsc0JB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/local-store.d.ts b/packages/loopover-miner/lib/local-store.d.ts deleted file mode 100644 index 391a52e222..0000000000 --- a/packages/loopover-miner/lib/local-store.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DatabaseSync } from "node:sqlite"; -import { type MinerD1Database, type SqliteDriver } from "./store-db-adapter.js"; -/** - * Resolve a local store's DB path from, in order: an explicit env var, `LOOPOVER_MINER_CONFIG_DIR`, - * `XDG_CONFIG_HOME` (falling back to `~/.config`) — mirroring every store's prior hand-written resolver. - */ -export declare function resolveLocalStoreDbPath(defaultDbFileName: string, explicitEnvVarName: string, env?: Record): string; -/** Trim and validate a caller-supplied (or resolved-default) DB path, throwing `invalidPathError` if it is empty. */ -export declare function normalizeLocalStoreDbPath(dbPath: string | null | undefined, resolvedDefault: string, invalidPathError: string): string; -/** - * Open (creating parent dirs on first use) a local store's SQLite file with 0700/0600 permissions and a shared - * busy-timeout, so two instances of the same store on one file serialize writes instead of racing. Skips the - * mkdir/chmod steps for the special `:memory:` path, which has no on-disk file. `run-state.js` previously opened - * its DB with no busy-timeout at all (the one inconsistency among the four stores this issue found); folding it - * through this shared helper gives it the same wait-don't-fail behavior the other three already had. - */ -export declare function openLocalStoreDb(resolvedPath: string, options?: { - busyTimeoutMs?: number; -}): DatabaseSync; -/** - * Open a local store through the #7175 SqliteDriver / D1 adapter seam. - * Returns the underlying DatabaseSync (for schema migrations / purge helpers that still take it), - * the sync SqliteDriver (preferred for store CRUD until a store goes fully async), and the async D1 - * adapter (same surface ORB uses — ready for a later createPgAdapter swap). - */ -export declare function openLocalStoreAdapter(resolvedPath: string, options?: { - busyTimeoutMs?: number; -}): { - db: DatabaseSync; - driver: SqliteDriver; - d1: MinerD1Database; -}; diff --git a/packages/loopover-miner/lib/local-store.js b/packages/loopover-miner/lib/local-store.js deleted file mode 100644 index 899b20ccb0..0000000000 --- a/packages/loopover-miner/lib/local-store.js +++ /dev/null @@ -1,80 +0,0 @@ -import { chmodSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import { registerCleanupResource } from "./process-lifecycle.js"; -import { createD1Adapter, nodeSqliteDriver } from "./store-db-adapter.js"; -// Shared path-resolution + DB-open boilerplate for the package's local SQLite stores (#4272). This is a DRY pass -// only, not a merge: run-state.js, claim-ledger.js, portfolio-queue.js, and event-ledger.js each keep their own -// `.sqlite3` file, table, and env var — this module just extracts the ~15 lines each hand-duplicated -// (env-var/config-dir/XDG path resolution, mkdirSync(0o700) + chmodSync(0o600), and `PRAGMA busy_timeout`). -// -// #7175 part 1 adds `openLocalStoreAdapter`: same open path, then wraps the handle as SqliteDriver + D1 adapter -// so stores can migrate onto the shared seam without changing self-host's node:sqlite default. -/** - * Resolve a local store's DB path from, in order: an explicit env var, `LOOPOVER_MINER_CONFIG_DIR`, - * `XDG_CONFIG_HOME` (falling back to `~/.config`) — mirroring every store's prior hand-written resolver. - */ -export function resolveLocalStoreDbPath(defaultDbFileName, explicitEnvVarName, env = process.env) { - const explicitPath = typeof env[explicitEnvVarName] === "string" ? env[explicitEnvVarName].trim() : ""; - if (explicitPath) - return explicitPath; - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" - ? env.LOOPOVER_MINER_CONFIG_DIR.trim() - : ""; - if (explicitConfigDir) - return join(explicitConfigDir, defaultDbFileName); - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultDbFileName); -} -/** Trim and validate a caller-supplied (or resolved-default) DB path, throwing `invalidPathError` if it is empty. */ -export function normalizeLocalStoreDbPath(dbPath, resolvedDefault, invalidPathError) { - const raw = dbPath ?? resolvedDefault; - if (typeof raw !== "string" || !raw.trim()) - throw new Error(invalidPathError); - return raw.trim(); -} -/** - * Open (creating parent dirs on first use) a local store's SQLite file with 0700/0600 permissions and a shared - * busy-timeout, so two instances of the same store on one file serialize writes instead of racing. Skips the - * mkdir/chmod steps for the special `:memory:` path, which has no on-disk file. `run-state.js` previously opened - * its DB with no busy-timeout at all (the one inconsistency among the four stores this issue found); folding it - * through this shared helper gives it the same wait-don't-fail behavior the other three already had. - */ -export function openLocalStoreDb(resolvedPath, options = {}) { - const busyTimeoutMs = options.busyTimeoutMs ?? 5000; - const isMemory = resolvedPath === ":memory:"; - if (!isMemory) - mkdirSync(dirname(resolvedPath), { recursive: true, mode: 0o700 }); - const db = new DatabaseSync(resolvedPath); - if (!isMemory) - chmodSync(resolvedPath, 0o600); - db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`); - // Crash-safety (#4826): register every opened store so a SIGINT/SIGTERM/uncaught-exception handler can close it - // mid-run instead of leaving it half-written. The normal `close()` unregisters first, so the happy path never - // double-closes and a long-running `loop` doesn't accumulate stale references. - const unregister = registerCleanupResource(db); - const originalClose = db.close.bind(db); - // Wrap close so cleanup registration is torn down with the handle. Assignment is intentional runtime behavior - // that the DatabaseSync type does not expose as writable. - db.close = () => { - unregister(); - return originalClose(); - }; - return db; -} -/** - * Open a local store through the #7175 SqliteDriver / D1 adapter seam. - * Returns the underlying DatabaseSync (for schema migrations / purge helpers that still take it), - * the sync SqliteDriver (preferred for store CRUD until a store goes fully async), and the async D1 - * adapter (same surface ORB uses — ready for a later createPgAdapter swap). - */ -export function openLocalStoreAdapter(resolvedPath, options = {}) { - const db = openLocalStoreDb(resolvedPath, options); - const driver = nodeSqliteDriver(db); - const d1 = createD1Adapter(driver); - return { db, driver, d1 }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9jYWwtc3RvcmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJsb2NhbC1zdG9yZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMvQyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQzFDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDakUsT0FBTyxFQUFFLGVBQWUsRUFBRSxnQkFBZ0IsRUFBMkMsTUFBTSx1QkFBdUIsQ0FBQztBQUVuSCxpSEFBaUg7QUFDakgsZ0hBQWdIO0FBQ2hILHFHQUFxRztBQUNyRyw0R0FBNEc7QUFDNUcsRUFBRTtBQUNGLGdIQUFnSDtBQUNoSCwrRkFBK0Y7QUFFL0Y7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLHVCQUF1QixDQUNyQyxpQkFBeUIsRUFDekIsa0JBQTBCLEVBQzFCLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBRXJELE1BQU0sWUFBWSxHQUFHLE9BQU8sR0FBRyxDQUFDLGtCQUFrQixDQUFDLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3ZHLElBQUksWUFBWTtRQUFFLE9BQU8sWUFBWSxDQUFDO0lBRXRDLE1BQU0saUJBQWlCLEdBQUcsT0FBTyxHQUFHLENBQUMseUJBQXlCLEtBQUssUUFBUTtRQUN6RSxDQUFDLENBQUMsR0FBRyxDQUFDLHlCQUF5QixDQUFDLElBQUksRUFBRTtRQUN0QyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ1AsSUFBSSxpQkFBaUI7UUFBRSxPQUFPLElBQUksQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/logger.d.ts b/packages/loopover-miner/lib/logger.d.ts deleted file mode 100644 index e0c89f84df..0000000000 --- a/packages/loopover-miner/lib/logger.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -export type LogLevel = "silent" | "error" | "warn" | "info" | "debug"; -/** Supported log levels, least to most verbose. `silent` suppresses everything. */ -export declare const LOG_LEVELS: readonly LogLevel[]; -/** The level used when nothing (flag, env var, or explicit option) selects one. */ -export declare const DEFAULT_LOG_LEVEL: LogLevel; -export interface LoggerStreams { - stdout?: { - write(chunk: string): unknown; - } | undefined; - stderr?: { - write(chunk: string): unknown; - } | undefined; -} -export interface LoggerOptions { - level?: string | undefined; - quiet?: boolean | undefined; - verbose?: boolean | undefined; - pretty?: boolean | undefined; - fields?: Record | undefined; - env?: Record | undefined; - streams?: LoggerStreams | undefined; - now?: (() => string) | undefined; -} -export interface Logger { - level: LogLevel; - isLevelEnabled(level: string): boolean; - error(message: string, fields?: Record): void; - warn(message: string, fields?: Record): void; - info(message: string, fields?: Record): void; - debug(message: string, fields?: Record): void; - child(fields: Record): Logger; -} -/** True when `value` names a supported log level. Non-string input is never a level (so an absent option or a - * typo'd env var falls through to the next signal instead of throwing). */ -export declare function isLogLevel(value: unknown): value is LogLevel; -/** - * Resolve the active level from the available signals, most explicit first: an explicit `level` wins, then - * `--quiet` (→ `error`), then `--verbose` (→ `debug`), then the env-provided level, else the default. `quiet` - * beats `verbose` when both are set, so the safer/quieter choice wins a contradictory invocation. An - * unrecognized `level`/`envLevel` is ignored rather than throwing — a typo logs at the default, never crashes. - */ -export declare function resolveLogLevel({ level, quiet, verbose, envLevel, }?: { - level?: string | undefined; - quiet?: boolean | undefined; - verbose?: boolean | undefined; - envLevel?: string | undefined; -}): LogLevel; -/** - * Split the global logging flags out of a CLI argv slice, returning the parsed options plus `rest` — the argv - * with those flags (and any `--log-level` value) removed so downstream command parsing never sees them. - * Recognizes `--quiet`, `--verbose`, `--log-level `, and `--log-level=`. No short aliases: `-v` - * is already `--version` and `-h` is `--help` in the CLI entrypoint. - */ -export declare function extractLogOptions(argv: string[]): { - options: { - quiet: boolean; - verbose: boolean; - level: string | undefined; - }; - rest: string[]; -}; -/** - * Render structured fields as a stable, sorted ` key=value` suffix (sorted so output is deterministic across - * runs). `undefined` values are dropped; an empty/absent field set yields an empty string. - */ -export declare function formatFields(fields?: Record | null | undefined): string; -/** - * Format one log line. Plain mode (the default) is just `message` + any field suffix, keeping human CLI output - * identical to a bare `console.log`. Pretty mode prefixes an optional timestamp and the uppercased level tag, - * for operators who want machine-scannable diagnostics. - */ -export declare function formatLine({ level, message, fields, pretty, timestamp, }: { - level: string; - message: string; - fields?: Record | null | undefined; - pretty?: boolean | undefined; - timestamp?: string | undefined; -}): string; -/** - * Build a level-aware logger. All I/O is injectable for tests: `streams` (defaults to process stdout/stderr), - * `now` (defaults to an ISO-8601 clock, only consulted in `pretty` mode), and `env` (defaults to process.env, - * read for `LOOPOVER_MINER_LOG_LEVEL`). `fields` seeds every line with contextual fields; `child(extra)` - * returns a logger that merges additional fields onto this one. - */ -export declare function createLogger(options?: LoggerOptions): Logger; -/** Reconfigure the process-wide logger from resolved startup options and return it. */ -export declare function configureLogger(options?: LoggerOptions): Logger; -/** The process-wide logger configured by `configureLogger` (a default-level logger before then). */ -export declare function getLogger(): Logger; diff --git a/packages/loopover-miner/lib/logger.js b/packages/loopover-miner/lib/logger.js deleted file mode 100644 index af3c1e76d1..0000000000 --- a/packages/loopover-miner/lib/logger.js +++ /dev/null @@ -1,153 +0,0 @@ -// Level-aware logging abstraction for the miner CLI (#4835): every CLI file previously reached for ad hoc -// `console.log`/`console.error` with no shared level control, so an operator could neither quiet routine -// chatter nor turn on verbose diagnostics. This module is the one dependency-light logger the CLI configures -// once at startup and every command shares. It is deliberately pure/injectable — `streams`, `now`, and `env` -// are all overridable — so the branchy level/format logic is unit-testable without touching real stdio. -// -// Levels are ordered by severity; a logger at level L emits a method only when the method's severity rank is at -// or below L's rank (so `error` always survives except at `silent`, and `debug` only shows at the most verbose -// setting). `error`/`warn` go to stderr, `info`/`debug` to stdout, matching the existing convention where the -// update-check nudge writes to stderr and normal command output writes to stdout. -/** Supported log levels, least to most verbose. `silent` suppresses everything. */ -export const LOG_LEVELS = ["silent", "error", "warn", "info", "debug"]; -/** The level used when nothing (flag, env var, or explicit option) selects one. */ -export const DEFAULT_LOG_LEVEL = "info"; -// Numeric severity rank per level (higher = more verbose). A method emits when its rank <= the active rank. -const LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 }; -const defaultClock = () => new Date().toISOString(); -/** True when `value` names a supported log level. Non-string input is never a level (so an absent option or a - * typo'd env var falls through to the next signal instead of throwing). */ -export function isLogLevel(value) { - return typeof value === "string" && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value); -} -/** - * Resolve the active level from the available signals, most explicit first: an explicit `level` wins, then - * `--quiet` (→ `error`), then `--verbose` (→ `debug`), then the env-provided level, else the default. `quiet` - * beats `verbose` when both are set, so the safer/quieter choice wins a contradictory invocation. An - * unrecognized `level`/`envLevel` is ignored rather than throwing — a typo logs at the default, never crashes. - */ -export function resolveLogLevel({ level, quiet = false, verbose = false, envLevel, } = {}) { - if (isLogLevel(level)) - return level; - if (quiet) - return "error"; - if (verbose) - return "debug"; - if (isLogLevel(envLevel)) - return envLevel; - return DEFAULT_LOG_LEVEL; -} -/** - * Split the global logging flags out of a CLI argv slice, returning the parsed options plus `rest` — the argv - * with those flags (and any `--log-level` value) removed so downstream command parsing never sees them. - * Recognizes `--quiet`, `--verbose`, `--log-level `, and `--log-level=`. No short aliases: `-v` - * is already `--version` and `-h` is `--help` in the CLI entrypoint. - */ -export function extractLogOptions(argv) { - let quiet = false; - let verbose = false; - let level; - const rest = []; - for (let index = 0; index < argv.length; index += 1) { - // noUncheckedIndexedAccess: in-bounds access is always defined at runtime. - const arg = argv[index]; - if (arg === "--quiet") { - quiet = true; - continue; - } - if (arg === "--verbose") { - verbose = true; - continue; - } - if (arg === "--log-level") { - level = argv[index + 1]; - index += 1; - continue; - } - if (arg.startsWith("--log-level=")) { - level = arg.slice("--log-level=".length); - continue; - } - rest.push(arg); - } - return { options: { quiet, verbose, level }, rest }; -} -function formatFieldValue(value) { - // Quote a string only when it contains whitespace (so it stays one token); serialize everything else as JSON. - if (typeof value === "string") - return /\s/.test(value) ? JSON.stringify(value) : value; - return JSON.stringify(value); -} -/** - * Render structured fields as a stable, sorted ` key=value` suffix (sorted so output is deterministic across - * runs). `undefined` values are dropped; an empty/absent field set yields an empty string. - */ -export function formatFields(fields) { - if (!fields) - return ""; - const parts = []; - for (const key of Object.keys(fields).sort()) { - const value = fields[key]; - if (value === undefined) - continue; - parts.push(`${key}=${formatFieldValue(value)}`); - } - return parts.length > 0 ? ` ${parts.join(" ")}` : ""; -} -/** - * Format one log line. Plain mode (the default) is just `message` + any field suffix, keeping human CLI output - * identical to a bare `console.log`. Pretty mode prefixes an optional timestamp and the uppercased level tag, - * for operators who want machine-scannable diagnostics. - */ -export function formatLine({ level, message, fields, pretty, timestamp, }) { - const suffix = formatFields(fields); - if (!pretty) - return `${message}${suffix}`; - const stamp = timestamp ? `[${timestamp}] ` : ""; - return `${stamp}${level.toUpperCase()} ${message}${suffix}`; -} -/** - * Build a level-aware logger. All I/O is injectable for tests: `streams` (defaults to process stdout/stderr), - * `now` (defaults to an ISO-8601 clock, only consulted in `pretty` mode), and `env` (defaults to process.env, - * read for `LOOPOVER_MINER_LOG_LEVEL`). `fields` seeds every line with contextual fields; `child(extra)` - * returns a logger that merges additional fields onto this one. - */ -export function createLogger(options = {}) { - const { level, quiet, verbose, pretty = false, fields: baseFields, env = process.env, streams, now } = options; - const stdout = streams?.stdout ?? process.stdout; - const stderr = streams?.stderr ?? process.stderr; - const clock = now ?? defaultClock; - const envLevel = env.LOOPOVER_MINER_LOG_LEVEL ?? ""; - const activeLevel = resolveLogLevel({ level, quiet, verbose, envLevel }); - const threshold = LEVEL_RANK[activeLevel]; - function emit(methodLevel, stream, message, fields) { - if (LEVEL_RANK[methodLevel] > threshold) - return; - const merged = baseFields || fields ? { ...baseFields, ...fields } : undefined; - const timestamp = pretty ? clock() : undefined; - stream.write(`${formatLine({ level: methodLevel, message, fields: merged, pretty, timestamp })}\n`); - } - return { - level: activeLevel, - // Cast: public API takes `string`; unknown levels are undefined in LEVEL_RANK, and `undefined <= n` is false. - isLevelEnabled: (methodLevel) => LEVEL_RANK[methodLevel] <= threshold, - error: (message, fields) => emit("error", stderr, message, fields), - warn: (message, fields) => emit("warn", stderr, message, fields), - info: (message, fields) => emit("info", stdout, message, fields), - debug: (message, fields) => emit("debug", stdout, message, fields), - child: (childFields) => createLogger({ ...options, fields: { ...baseFields, ...childFields } }), - }; -} -// Process-wide logger. The CLI entrypoint calls `configureLogger` once from the parsed global flags/env so every -// command shares one configured instance via `getLogger`; until then this default-level instance is used. -let processLogger = createLogger(); -/** Reconfigure the process-wide logger from resolved startup options and return it. */ -export function configureLogger(options) { - processLogger = createLogger(options); - return processLogger; -} -/** The process-wide logger configured by `configureLogger` (a default-level logger before then). */ -export function getLogger() { - return processLogger; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9nZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9nZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDBHQUEwRztBQUMxRyx5R0FBeUc7QUFDekcsNkdBQTZHO0FBQzdHLDZHQUE2RztBQUM3Ryx3R0FBd0c7QUFDeEcsRUFBRTtBQUNGLGdIQUFnSDtBQUNoSCwrR0FBK0c7QUFDL0csOEdBQThHO0FBQzlHLGtGQUFrRjtBQUlsRixtRkFBbUY7QUFDbkYsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUF3QixDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQztBQUU1RixtRkFBbUY7QUFDbkYsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQWEsTUFBTSxDQUFDO0FBRWxELDRHQUE0RztBQUM1RyxNQUFNLFVBQVUsR0FBNkIsRUFBRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLEtBQUssRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLEtBQUssRUFBRSxDQUFDLEVBQUUsQ0FBQztBQUVqRyxNQUFNLFlBQVksR0FBRyxHQUFXLEVBQUUsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO0FBNEI1RDs0RUFDNEU7QUFDNUUsTUFBTSxVQUFVLFVBQVUsQ0FBQyxLQUFjO0lBQ3ZDLE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FBQyxFQUM5QixLQUFLLEVBQ0wsS0FBSyxHQUFHLEtBQUssRUFDYixPQUFPLEdBQUcsS0FBSyxFQUNmLFFBQVEsTUFNTixFQUFFO0lBQ0osSUFBSSxVQUFVLENBQUMsS0FBSyxDQUFDO1FBQUUs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/loop-cli.d.ts b/packages/loopover-miner/lib/loop-cli.d.ts deleted file mode 100644 index 68dfde88b0..0000000000 --- a/packages/loopover-miner/lib/loop-cli.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { GovernorState } from "./governor-state.js"; -import type { GovernorLedger } from "./governor-ledger.js"; -import type { EventLedger } from "./event-ledger.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -import type { RunStateStore } from "./run-state.js"; -import type { AttemptCliResult } from "./attempt-cli.js"; -import type { PollPrDispositionOptions } from "./pr-disposition-poller.js"; -import type { CheckRunConclusion, PollCheckRunsOptions } from "./ci-poller.js"; -export type ParsedLoopArgs = { - error: string; -} | { - targets: string[]; - search: string | null; - minerLogin: string; - base: string; - live: boolean; - dryRun: boolean; - maxCycles: number | undefined; - cycleDelayMs: number; - json: boolean; -}; -export type LoopCycleSummary = { - cycle: number; - outcome: "idle_queue_empty" | "halted" | "attempted" | "skipped_malformed_identifier"; - reason?: string; - repoFullName?: string; - identifier?: string; - attemptOutcome?: AttemptCliResult["outcome"] | "attempt_error"; - reentryOutcome?: "merged" | "disengaged" | "other"; - prNumber?: number | null; - ciConclusion?: CheckRunConclusion | null; - reentered?: boolean; - reasons?: string[]; -}; -export type RunLoopOptions = { - env?: Record; - nowMs?: number; - githubToken?: string; - apiBaseUrl?: string; - sleepFn?: (delayMs: number) => Promise; - openGovernorState?: () => GovernorState; - initEventLedger?: () => EventLedger; - initGovernorLedger?: () => GovernorLedger; - initPortfolioQueue?: () => PortfolioQueueStore; - initRunStateStore?: () => RunStateStore; - runDiscover?: (args: string[], options?: Record) => Promise; - runAttempt?: (args: string[], options?: Record) => Promise; - resolveAmsPolicy?: (repoFullName: string, options?: Record) => Promise<{ - spec: Record; - source: string; - warnings: string[]; - }>; - checkMinerKillSwitch?: (input?: { - env?: Record; - repoPaused?: boolean; - }) => { - scope: "global" | "repo" | "none"; - active: boolean; - }; - evaluateRunLoopBoundaryGate?: (input: unknown, options?: unknown) => { - verdict: { - reason: string; - }; - canClaimNext: boolean; - }; - pollPrDisposition?: (repoFullName: string, prNumber: number, options?: PollPrDispositionOptions) => Promise<{ - state: "open" | "closed"; - merged: boolean; - closedAt: string | null; - attempts: number; - }>; - pollCheckRuns?: (repoFullName: string, prNumber: number, options?: PollCheckRunsOptions) => Promise<{ - conclusion: CheckRunConclusion; - checks: unknown[]; - headSha: string; - attempts: number; - }>; - recordPrOutcomeSnapshot?: (input: unknown, options?: unknown) => unknown; - buildLoopClosureSummary?: (sources: unknown, options?: unknown) => { - sinceSeq: number | null; - lastSeq: number; - }; - attemptLoopReentry?: (candidate: unknown, deps: unknown) => { - decision: { - reenter: boolean; - reasons: string[]; - }; - dequeued: { - repoFullName: string; - identifier: string; - priority: number; - status: string; - enqueuedAt: string; - } | null; - }; - attemptOptions?: Record; - prDispositionOptions?: PollPrDispositionOptions; - ciPollOptions?: PollCheckRunsOptions; -}; -export declare function parseLoopArgs(args: string[]): ParsedLoopArgs; -/** - * Run one full discover -> claim -> attempt -> observe -> reenter cycle repeatedly until a kill-switch trips, - * the run-loop boundary gate halts (non-convergence or a real budget/turn/elapsed cap), re-entry is declined, - * or `--max-cycles` is reached. Fails closed: refuses to start at all if governor state cannot be loaded. - */ -export declare function runLoop(args: string[], options?: RunLoopOptions): Promise; diff --git a/packages/loopover-miner/lib/loop-cli.js b/packages/loopover-miner/lib/loop-cli.js deleted file mode 100644 index 2dc48b7156..0000000000 --- a/packages/loopover-miner/lib/loop-cli.js +++ /dev/null @@ -1,532 +0,0 @@ -// The autonomous supervising loop (#5135, Wave 3.5): the missing daemon/watch layer over the one-shot -// `discover`/`attempt` subcommands. Every existing piece it composes -- runDiscover, runAttempt, -// evaluateRunLoopBoundaryGate, attemptLoopReentry, buildLoopClosureSummary, governor-state.js -- already -// existed; this is the first caller that actually chains them into a real repeat-until-halted run. -// -// STRUCTURE (one cycle): kill-switch check -> pause-flag check (#4851, governor-state.js's persisted -// paused/reason/pausedAt) -> real-per-repo-policy-aware run-loop boundary gate (before claiming) -> real -// runAttempt -> real CI-status poll (ci-poller.js, #5394) + real PR-disposition poll -// (pr-disposition-poller.js, on a submitted outcome) -> real loop-closure summary -> real attemptLoopReentry -// decision. `attemptLoopReentry`'s own dequeue is the -// AUTHORITATIVE claim for every cycle after the first (its own doc: "if allowed -- dequeues the next -// candidate") -- this loop does not ALSO call portfolioQueue.dequeueNext() on a successful reentry, which -// would silently double-claim (the reentry's own claim would then leak as a permanently 'in_progress', never- -// attempted row). A manual dequeueNext() is used only to prime the very first cycle (no prior outcome exists -// yet to reenter from) and to refill after an empty queue. -// -// REAL, NOT FABRICATED: this loop is the first production caller of governor-state.js's `saveCapUsage` -// (turnsTaken from runMinerAttempt's own real `loopResult.totalTurnsUsed`, elapsedMs from real wall-clock -// measurement). Its per-identifier convergence history (attempts/consecutiveFailures/reenqueues) is the real, -// SQLite-persisted portfolio-queue attempt-history (portfolio-queue.js's getAttemptHistory, #5654) that the -// dequeueNext claim + markDone/markFailed calls below already maintain -- the same source a one-shot `attempt` -// invocation reads (#5654), so both share one source of truth and the counters survive a loop-daemon restart -// (crash/deploy/systemd bounce) instead of resetting with the process (#5677). -import { checkMinerKillSwitch } from "./governor-kill-switch.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { evaluateRunLoopBoundaryGate } from "./governor-run-halt.js"; -import { openGovernorState } from "./governor-state.js"; -import { initGovernorLedger } from "./governor-ledger.js"; -import { initEventLedger } from "./event-ledger.js"; -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { initRunStateStore } from "./run-state.js"; -import { runDiscover } from "./discover-cli.js"; -import { runAttempt } from "./attempt-cli.js"; -import { resolveAmsPolicy } from "./ams-policy.js"; -import { pollPrDisposition, classifyPrDisposition } from "./pr-disposition-poller.js"; -import { pollCheckRuns } from "./ci-poller.js"; -import { recordPrOutcomeSnapshot } from "./pr-outcome.js"; -import { isRejectedPr } from "./rejection-state-machine.js"; -import { buildLoopClosureSummary } from "./loop-closure.js"; -import { attemptLoopReentry } from "./loop-reentry.js"; -import { parsePrNumberFromExecResult } from "./pr-number-parse.js"; -import { resolveGitHubToken } from "./github-token-resolution.js"; -import { DEFAULT_AMS_POLICY_SPEC } from "@loopover/engine"; -const LOOP_USAGE = "Usage: loopover-miner loop [...] | --search --miner-login [--base ] [--live] [--dry-run] [--max-cycles ] [--cycle-delay-ms ] [--json]"; -const DEFAULT_CYCLE_DELAY_MS = 60_000; -const ISSUE_IDENTIFIER_PATTERN = /^issue:(\d+)$/; -function parseRepoTarget(value) { - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - return `${owner}/${repo}`; -} -function normalizeOptionalPositiveInt(value, label) { - const parsedValue = Number(value); - if (!Number.isFinite(parsedValue) || !Number.isInteger(parsedValue) || parsedValue < 0) { - throw new Error(`${label} must be a non-negative integer: ${value}`); - } - return parsedValue; -} -export function parseLoopArgs(args) { - const options = { - json: false, - minerLogin: null, - base: "main", - live: false, - dryRun: false, - search: null, - maxCycles: undefined, - cycleDelayMs: DEFAULT_CYCLE_DELAY_MS, - }; - const targets = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--live") { - options.live = true; - continue; - } - // #4847: see attempt-cli.js's own --dry-run comment -- distinct from --live's absence, this short-circuits - // BEFORE governor state or any other store is opened, guaranteeing zero discovery/queue/ledger writes. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--search") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: LOOP_USAGE }; - options.search = value; - index += 1; - continue; - } - if (token === "--miner-login") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: LOOP_USAGE }; - options.minerLogin = value; - index += 1; - continue; - } - if (token === "--base") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: LOOP_USAGE }; - options.base = value; - index += 1; - continue; - } - if (token === "--max-cycles") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: LOOP_USAGE }; - try { - options.maxCycles = normalizeOptionalPositiveInt(value, "--max-cycles"); - } - catch (error) { - return { error: describeCliError(error) }; - } - index += 1; - continue; - } - if (token === "--cycle-delay-ms") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: LOOP_USAGE }; - try { - options.cycleDelayMs = normalizeOptionalPositiveInt(value, "--cycle-delay-ms"); - } - catch (error) { - return { error: describeCliError(error) }; - } - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - const target = parseRepoTarget(token); - if (!target) - return { error: `Repository must be in owner/repo form: ${token}` }; - targets.push(target); - } - if (options.search === null && targets.length === 0) - return { error: LOOP_USAGE }; - if (options.search !== null && targets.length > 0) - return { error: "Pass either repository targets or --search, not both." }; - if (!options.minerLogin) - return { error: `--miner-login is required. ${LOOP_USAGE}` }; - return { - targets, - search: options.search, - minerLogin: options.minerLogin, - base: options.base, - live: options.live, - dryRun: options.dryRun, - maxCycles: options.maxCycles, - cycleDelayMs: options.cycleDelayMs, - json: options.json, - }; -} -function discoverArgv(parsed) { - return parsed.search !== null ? ["--search", parsed.search] : [...parsed.targets]; -} -function parseIssueNumberFromIdentifier(identifier) { - const match = typeof identifier === "string" ? identifier.match(ISSUE_IDENTIFIER_PATTERN) : null; - return match ? Number(match[1]) : null; -} -function defaultSleep(delayMs) { - return new Promise((resolve) => setTimeout(resolve, delayMs)); -} -/** - * Run one full discover -> claim -> attempt -> observe -> reenter cycle repeatedly until a kill-switch trips, - * the run-loop boundary gate halts (non-convergence or a real budget/turn/elapsed cap), re-entry is declined, - * or `--max-cycles` is reached. Fails closed: refuses to start at all if governor state cannot be loaded. - */ -export async function runLoop(args, options = {}) { - const parsed = parseLoopArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - // Narrow for nested closures (TS resets control-flow narrowing inside nested functions). - const loopArgs = parsed; - const env = options.env ?? process.env; - const sleepFn = options.sleepFn ?? defaultSleep; - const nowMsFn = () => options.nowMs ?? Date.now(); - const sessionStartMs = nowMsFn(); - // #4847: reports what a real loop invocation would target and returns BEFORE governor state or any other - // store (event/governor ledger, portfolio queue, run state) is opened -- a provable zero-write path, not just - // "opened but didn't write." The loop's own discovery call enqueues newly-found candidates into the LOCAL - // portfolio queue even before any attempt happens, so a faithful dry run cannot call it either. - if (parsed.dryRun) { - const dryRunResult = { - outcome: "dry_run", - targets: parsed.targets, - search: parsed.search, - minerLogin: parsed.minerLogin, - base: parsed.base, - live: parsed.live, - maxCycles: parsed.maxCycles ?? null, - }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - const target = parsed.search !== null ? `--search ${parsed.search}` : parsed.targets.join(", "); - console.log(`DRY RUN: would run an autonomous loop against ${target} for ${parsed.minerLogin} (base: ${parsed.base}, live: ${parsed.live}). No discovery, queue, or ledger writes were made.`); - } - return 0; - } - let governorState; - try { - governorState = (options.openGovernorState ?? openGovernorState)(); - } - catch (error) { - return reportCliFailure(parsed.json, `Loop refuses to start: governor state cannot be loaded: ${describeCliError(error)}`, 3); - } - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - const governorLedger = (options.initGovernorLedger ?? initGovernorLedger)(); - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - const runState = (options.initRunStateStore ?? initRunStateStore)(); - const runDiscoverFn = options.runDiscover ?? runDiscover; - const runAttemptFn = options.runAttempt ?? runAttempt; - const resolveAmsPolicyFn = options.resolveAmsPolicy ?? resolveAmsPolicy; - const checkKillSwitchFn = options.checkMinerKillSwitch ?? checkMinerKillSwitch; - const evaluateBoundaryGateFn = options.evaluateRunLoopBoundaryGate ?? evaluateRunLoopBoundaryGate; - const pollPrDispositionFn = options.pollPrDisposition ?? pollPrDisposition; - const pollCheckRunsFn = options.pollCheckRuns ?? pollCheckRuns; - const recordPrOutcomeSnapshotFn = options.recordPrOutcomeSnapshot ?? recordPrOutcomeSnapshot; - const buildLoopClosureSummaryFn = options.buildLoopClosureSummary ?? buildLoopClosureSummary; - const attemptLoopReentryFn = options.attemptLoopReentry ?? attemptLoopReentry; - // Resolved ONCE, at the CLI-entrypoint layer, mirroring manage-poll.js's own runManagePoll (its - // recordManagePollSnapshot callee has no env fallback of its own either -- the top-level CLI function is - // where the GitHub token gets resolved, then threaded down explicitly to every real GitHub caller). - // pollPrDisposition (unlike runDiscover, which falls back to process.env.GITHUB_TOKEN internally) has NO - // such fallback -- an unresolved githubToken here would silently poll unauthenticated. - // resolveGitHubToken (#6116): GITHUB_TOKEN env override wins outright, else a live token from the - // authenticated `loopover-mcp login` session -- cached in memory for this process's lifetime. - const githubToken = options.githubToken ?? (await resolveGitHubToken(env)) ?? ""; - async function runDiscoveryOnce() { - await runDiscoverFn(discoverArgv(loopArgs), { - initPortfolioQueue: () => portfolioQueue, - githubToken, - ...(options.apiBaseUrl !== undefined ? { apiBaseUrl: options.apiBaseUrl } : {}), - nowMs: nowMsFn(), - }); - } - let usage = governorState.loadCapUsage(); - const cycles = []; - let sinceSeq = eventLedger.readEvents({}).at(-1)?.seq ?? 0; - let haltReason = null; - try { - // Checked BEFORE any work at all -- including the very first discovery call -- so an already-active kill - // switch OR an already-active pause (#4851) halts the loop without ever touching GitHub or the queue. The - // pause flag is real, persisted, operator/governor-writable state on governorState (toggled via - // `loopover-miner governor pause`/`resume`) -- unlike the kill switch, a paused run resumes simply by being - // re-invoked: every piece of per-cycle state this loop reads (portfolioQueue, runState, governorState's own - // cap usage) is already durable, so clearing the flag and restarting continues exactly where it left off. - const initialKillSwitch = checkKillSwitchFn({ env }); - const initialPauseState = governorState.loadPauseState(); - let claimed = null; - if (initialKillSwitch.active) { - haltReason = `kill_switch_${initialKillSwitch.scope}`; - cycles.push({ cycle: 1, outcome: "halted", reason: haltReason }); - } - else if (initialPauseState.paused) { - haltReason = "paused"; - cycles.push({ cycle: 1, outcome: "halted", reason: haltReason }); - } - else { - await runDiscoveryOnce(); - claimed = portfolioQueue.dequeueNext(); - } - let cycleIndex = haltReason !== null ? 1 : 0; - while (haltReason === null && (parsed.maxCycles === undefined || cycleIndex < parsed.maxCycles)) { - cycleIndex += 1; - const killSwitch = checkKillSwitchFn({ env }); - if (killSwitch.active) { - haltReason = `kill_switch_${killSwitch.scope}`; - // Release the in-flight claim so left state is defined (#5670 / mirrors run-halt's markFailed). - if (claimed) { - portfolioQueue.markFailed(claimed.repoFullName, claimed.identifier, claimed.apiBaseUrl); - } - cycles.push({ - cycle: cycleIndex, - outcome: "halted", - reason: haltReason, - ...(claimed - ? { repoFullName: claimed.repoFullName, identifier: claimed.identifier } - : {}), - }); - break; - } - const pauseState = governorState.loadPauseState(); - if (pauseState.paused) { - haltReason = "paused"; - if (claimed) { - portfolioQueue.markFailed(claimed.repoFullName, claimed.identifier, claimed.apiBaseUrl); - } - cycles.push({ - cycle: cycleIndex, - outcome: "halted", - reason: haltReason, - ...(claimed - ? { repoFullName: claimed.repoFullName, identifier: claimed.identifier } - : {}), - }); - break; - } - if (!claimed) { - cycles.push({ cycle: cycleIndex, outcome: "idle_queue_empty" }); - await sleepFn(parsed.cycleDelayMs); - await runDiscoveryOnce(); - claimed = portfolioQueue.dequeueNext(); - continue; - } - const issueNumber = parseIssueNumberFromIdentifier(claimed.identifier); - if (issueNumber === null) { - // Never produced by enqueueRankedDiscovery in practice (always "issue:N") -- fail soft rather than - // crash the whole run: this exact item can never be attempted, so it will never resolve on retry. - portfolioQueue.markDone(claimed.repoFullName, claimed.identifier, claimed.apiBaseUrl); - cycles.push({ cycle: cycleIndex, outcome: "skipped_malformed_identifier", identifier: claimed.identifier }); - claimed = portfolioQueue.dequeueNext(); - continue; - } - // Capture for the boundary-gate markFailed callback (claimed is reassigned later in the loop). - const claimedEntry = claimed; - const amsPolicy = await resolveAmsPolicyFn(claimedEntry.repoFullName, { env }); - // Real, SQLite-persisted per-item convergence history (#5677): the dequeueNext claim above already recorded - // this attempt and the markDone/markFailed calls below record the outcome, so reading it back here shares one - // source of truth with attempt-cli.js (#5654) and survives a loop-daemon restart instead of resetting. - const convergenceInput = portfolioQueue.getAttemptHistory(claimedEntry.repoFullName, claimedEntry.identifier, claimedEntry.apiBaseUrl); - // RunLoopOptions.resolveAmsPolicy types spec as Record; fall back when fields are absent. - const limits = amsPolicy.spec.capLimits ?? - DEFAULT_AMS_POLICY_SPEC.capLimits; - const convergenceThresholds = amsPolicy.spec.convergenceThresholds ?? - DEFAULT_AMS_POLICY_SPEC.convergenceThresholds; - const boundary = evaluateBoundaryGateFn({ - runHalted: false, - usage, - limits, - convergence: convergenceInput, - convergenceThresholds, - inFlightItem: { repoFullName: claimedEntry.repoFullName, identifier: claimedEntry.identifier }, - // Echoes claimed.apiBaseUrl (#5563), NOT the callback's own repoFullName/identifier alone -- two forge - // hosts can share an in-flight item with the same repo name+identifier. - markFailed: (repoFullName, identifier) => portfolioQueue.markFailed(repoFullName, identifier, claimedEntry.apiBaseUrl), - }, { append: (event) => governorLedger.appendGovernorEvent(event) }); - if (!boundary.canClaimNext) { - haltReason = `boundary_${boundary.verdict.reason}`; - cycles.push({ cycle: cycleIndex, outcome: "halted", reason: haltReason, repoFullName: claimedEntry.repoFullName, identifier: claimedEntry.identifier }); - break; - } - const cycleStartMs = nowMsFn(); - // Local result bag: AttemptCliResult is a discriminant union; CFA after the onResult callback - // collapses typed bags to `never`, so keep this local untyped (runtime shape unchanged). - let lastResult = null; - const attemptArgv = [ - claimedEntry.repoFullName, - String(issueNumber), - "--miner-login", - parsed.minerLogin, - "--base", - parsed.base, - ...(parsed.live ? ["--live"] : []), - ]; - await runAttemptFn(attemptArgv, { - ...(options.attemptOptions ?? {}), - env, - onResult: (result) => { - lastResult = result; - }, - }); - const cycleElapsedMs = nowMsFn() - cycleStartMs; - usage = { - // Real for the agent-sdk provider (its own SDK result message reports total_cost_usd, wired through - // runMinerAttempt's real loopResult.totalCostUsd); the CLI-subprocess providers (claude-cli/codex-cli) - // report no cost signal today, so this contributes 0 for those runs -- an honest absence, not a - // fabricated number. A capLimits.budget dimension only ever meaningfully trips against agent-sdk spend. - budgetSpent: usage.budgetSpent + (lastResult?.totalCostUsd ?? 0), - turnsTaken: usage.turnsTaken + (lastResult?.totalTurnsUsed ?? 0), - elapsedMs: usage.elapsedMs + cycleElapsedMs, - }; - governorState.saveCapUsage(usage); - const attemptOutcome = lastResult?.outcome ?? "attempt_error"; - const submitted = attemptOutcome === "attempt_submitted"; - // A repo-wide AI-usage-policy ban will never resolve on retry -- stop re-queuing it (matches - // rejection-signal.js's own "this repo bans automated contributions" semantics). Every other blocked/ - // abandoned/stale/governed outcome MAY resolve on a later retry (transient infra, contention, a - // different iteration budget) and is requeued -- a genuinely stuck item is caught by non-convergence - // (reenqueues threshold) rather than silently retried forever. - const permanentBlock = attemptOutcome === "blocked_rejection_signaled"; - // Mid-attempt kill-switch abandon (#5670): stop the outer loop immediately instead of waiting for the - // next between-cycle probe, and treat the item like any other re-queued abandon via markFailed below. - const killSwitchAbandon = lastResult?.abandonReason === "kill_switch_engaged"; - if (submitted || permanentBlock) { - // Both terminal -- a submitted PR is done, and a repo-wide AI-usage-policy ban never resolves on retry -- - // so neither is re-queued. markDone also clears the persisted consecutive-failure streak. - portfolioQueue.markDone(claimedEntry.repoFullName, claimedEntry.identifier, claimedEntry.apiBaseUrl); - } - else { - // Any other blocked/abandoned/stale/governed outcome may resolve on a later retry, so requeue it; markFailed - // records the re-enqueue + consecutive failure the non-convergence detector reads on the next cycle. - portfolioQueue.markFailed(claimedEntry.repoFullName, claimedEntry.identifier, claimedEntry.apiBaseUrl); - } - if (killSwitchAbandon) { - const liveKill = checkKillSwitchFn({ env }); - haltReason = liveKill.active ? `kill_switch_${liveKill.scope}` : "kill_switch_engaged"; - cycles.push({ - cycle: cycleIndex, - outcome: "halted", - reason: haltReason, - repoFullName: claimedEntry.repoFullName, - identifier: claimedEntry.identifier, - attemptOutcome, - }); - break; - } - let reentryOutcome = "other"; - let prNumber = null; - let prDisposition = null; - let ciConclusion = null; - if (submitted) { - prNumber = parsePrNumberFromExecResult(lastResult?.execResult, claimedEntry.repoFullName); - if (prNumber !== null) { - // Real CI-status observation (#5394): recorded BEFORE the disposition poll below, so a submitted - // PR's check-run state is captured even while it's still open, not just at its eventual merge/close. - // ci-poller.js's real GitHub check-run polling is a heuristic proxy for the gate verdict; the - // authoritative terminal merge/close outcome comes from pollPrDispositionFn below, sourced directly - // from GitHub's own PR state rather than a server-internal endpoint (#5450). - const ciStatus = await pollCheckRunsFn(claimedEntry.repoFullName, prNumber, { - githubToken, - ...(options.apiBaseUrl !== undefined ? { apiBaseUrl: options.apiBaseUrl } : {}), - ...(options.ciPollOptions ?? {}), - }); - ciConclusion = ciStatus.conclusion; - eventLedger.appendEvent({ - type: "ci_status_observed", - repoFullName: claimedEntry.repoFullName, - payload: { prNumber, conclusion: ciStatus.conclusion, checkCount: ciStatus.checks.length, source: "ci-poller" }, - }); - prDisposition = await pollPrDispositionFn(claimedEntry.repoFullName, prNumber, { - githubToken, - ...(options.apiBaseUrl !== undefined ? { apiBaseUrl: options.apiBaseUrl } : {}), - ...(options.prDispositionOptions ?? {}), - }); - if (prDisposition.state === "closed") { - recordPrOutcomeSnapshotFn({ - repoFullName: claimedEntry.repoFullName, - prNumber, - decision: prDisposition.merged ? "merged" : "closed", - closedAt: prDisposition.closedAt, - }, { eventLedger }); - // Real per-repo reputation history (#5675): a resolved terminal outcome updates the decided/unfavorable - // counts the Governor's self-reputation throttle reads on this repo's next attempt. `decided` always; - // `unfavorable` only on a closed-without-merge (rejection-state-machine.js's isRejectedPr, matching - // #5655's own-rejection classification). Forge-scoped by claimed.apiBaseUrl (#5563), like every other - // governor-state write here. - const priorReputation = governorState.loadReputationHistory(claimed.repoFullName, claimed.apiBaseUrl); - governorState.saveReputationHistory(claimed.repoFullName, { - decided: priorReputation.decided + 1, - unfavorable: priorReputation.unfavorable + (isRejectedPr(prDisposition) ? 1 : 0), - }, claimed.apiBaseUrl); - reentryOutcome = classifyPrDisposition(prDisposition); - } - } - } - const loopSummary = buildLoopClosureSummaryFn({ eventLedger, portfolioQueue, runState }, { sinceSeq, repoFullName: claimed.repoFullName }); - sinceSeq = loopSummary.lastSeq; - const reentry = attemptLoopReentryFn({ killSwitchScope: killSwitch.scope, repoFullName: claimed.repoFullName, outcome: reentryOutcome }, { eventLedger, portfolioQueue, runState, nowMs: nowMsFn(), sessionStartMs, loopSummary }); - cycles.push({ - cycle: cycleIndex, - outcome: "attempted", - repoFullName: claimed.repoFullName, - identifier: claimed.identifier, - attemptOutcome, - reentryOutcome, - prNumber, - ciConclusion, - reentered: reentry.decision.reenter, - reasons: reentry.decision.reasons, - }); - if (!reentry.decision.reenter) { - haltReason = `reentry_declined:${reentry.decision.reasons.join(",")}`; - break; - } - if (reentry.dequeued) { - // attemptLoopReentry's injectable .d.ts types dequeued.status as string; QueueEntry wants QueueStatus. - claimed = reentry.dequeued; - await sleepFn(parsed.cycleDelayMs); - } - else { - await sleepFn(parsed.cycleDelayMs); - await runDiscoveryOnce(); - claimed = portfolioQueue.dequeueNext(); - } - } - if (haltReason === null && parsed.maxCycles !== undefined) { - haltReason = "max_cycles_reached"; - // The next cycle's item is primed (dequeued → 'in_progress') BEFORE the while-condition re-checks - // maxCycles -- both at the initial priming above and at each cycle's tail -- so exhausting maxCycles - // ends the run holding a claim no cycle ever processed. Release it, mirroring the kill-switch/pause - // halts (#5670): dequeueNext() only pulls 'queued' rows, so an unreleased claim is invisible to every - // future loop/attempt run until an out-of-band stale-lease sweep reclaims it. - if (claimed) { - portfolioQueue.markFailed(claimed.repoFullName, claimed.identifier, claimed.apiBaseUrl); - } - } - // After the max-cycles release block above, haltReason is always set on a clean exit. - const summary = { haltReason, cyclesRun: cycles.length, cycles }; - if (parsed.json) { - console.log(JSON.stringify(summary, null, 2)); - } - else { - console.log(`Loop finished after ${cycles.length} cycle(s): ${haltReason}.`); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - governorState.close(); - eventLedger.close(); - governorLedger.close(); - portfolioQueue.close(); - runState.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcC1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJsb29wLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxzR0FBc0c7QUFDdEcsaUdBQWlHO0FBQ2pHLHlHQUF5RztBQUN6RyxtR0FBbUc7QUFDbkcsRUFBRTtBQUNGLHFHQUFxRztBQUNyRyx5R0FBeUc7QUFDekcscUZBQXFGO0FBQ3JGLDZHQUE2RztBQUM3RyxzREFBc0Q7QUFDdEQscUdBQXFHO0FBQ3JHLDBHQUEwRztBQUMxRyw4R0FBOEc7QUFDOUcsNkdBQTZHO0FBQzdHLDJEQUEyRDtBQUMzRCxFQUFFO0FBQ0YsdUdBQXVHO0FBQ3ZHLDBHQUEwRztBQUMxRyw4R0FBOEc7QUFDOUcsNEdBQTRHO0FBQzVHLCtHQUErRztBQUMvRyw2R0FBNkc7QUFDN0csK0VBQStFO0FBRS9FLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRixPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNyRSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUUxRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFcEQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFFL0QsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFbkQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUU5QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUscUJBQXFCLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUV0RixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFL0MsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDMUQsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzVELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzVELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3ZELE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ25FLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQ2xFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBMEQzRCxNQUFNLFVBQVUsR0FDZCwrTEFBK0wsQ0FBQztBQUNsTSxNQUFNLHNCQUFzQixHQUFHLE1BQU0sQ0FBQztBQUN0QyxNQUFNLHdCQUF3QixHQUFHLGVBQWUsQ0FBQztBQUVqRCxTQUFTLGVBQWUsQ0FBQyxLQUFhO0lBQ3BDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM3QixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN4RCxPQUFPLEdBQUcsS0FBSyxJQUFJLElBQUksRUFBRSxDQUFDO0FBQzVCLENBQUM7QUFFRCxTQUFTLDRCQUE0QixDQUFDLEtBQWMsRUFBRSxLQUFhO0lBQ2pFLE1BQU0sV0FBVyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNsQyxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLElBQUksV0FBVyxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3ZGLE1BQU0sSUFBSSxLQUFLLENBQUMsR0FBRyxLQUFLLG9DQUFvQyxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZFLENBQUM7SUFDRCxPQUFPLFdBQVcsQ0FBQztBQUNyQixDQUFDO0FBRUQsTUFBTSxVQUFVLGFBQWEsQ0FBQyxJQUFjO0lBQzFDLE1BQU0sT0FBTyxHQVNUO1FBQ0YsSUFBSSxFQUFFLEtBQUs7UUFDWCxVQUFVLEVBQUUsSUFBSTtRQUNoQixJQUFJLEVBQUUsTUFBTTtRQUNaLElBQUksRUFBRSxLQUFLO1FBQ1gsTUFBTSxFQUFFLEtBQUs7UUFDYixNQUFNLEVBQUUsSUFBSTtRQUNaLFNBQVMsRUFBRSxTQUFTO1FBQ3BCLFlBQVksRUFBRSxzQkFBc0I7S0FDckMsQ0FBQztJQUNGLE1BQU0sT0FBTyxHQUFhLEVBQUUsQ0FBQztJQUU3QixLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDcEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBRSxDQUFDO1FBQzNCLElBQUksS0FBSyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3ZCLE9BQU8sQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1lBQ3BCLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7WUFDcEIsU0FBUztRQUNYLENBQUM7UUFDRCwyR0FBMkc7UUFDM0csdUdBQXVHO1FBQ3ZHLElBQUksS0FBSyxLQUFLLFdBQVcsRUFBRSxDQUFDO1lBQzFCLE9BQU8sQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDO1lBQ3RCLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEtBQUssVUFBVSxFQUFFLENBQUM7WUFDekIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQztZQUM5QixJQUFJLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDO2dCQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLENBQUM7WUFDbEUsT0FBTyxDQUFDLE1BQU0sR0FBRyxLQUFLLENBQUM7WUFDdkIsS0FBSyxJQUFJLENBQUMsQ0FBQztZQUNYLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEtBQUssZUFBZSxFQUFFLENBQUM7WUFDOUIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQztZQUM5QixJQUFJLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDO2dCQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLENBQUM7WUFDbEUsT0FBTyxDQUFDLFVBQVUsR0FBRyxLQUFLLENBQUM7WUFDM0IsS0FBSyxJQUFJLENBQUMsQ0FBQztZQUNYLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQztZQUM5QixJQUFJLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDO2dCQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLENBQUM7WUFDbEUsT0FBTyxDQUFDLElBQUksR0FBRyxLQUFLLENBQUM7WUFDckIsS0FBSyxJQUFJLENBQUMsQ0FBQztZQUNYLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLEtBQUssY0FBYyxFQUFFLENBQUM7WUFDN0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/loop-closure.d.ts b/packages/loopover-miner/lib/loop-closure.d.ts deleted file mode 100644 index 1b2d5b5b26..0000000000 --- a/packages/loopover-miner/lib/loop-closure.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface LoopClosureEventLedger { - readEvents(filter?: { - since?: number; - repoFullName?: string; - }): Array<{ - seq?: number; - type?: unknown; - repoFullName?: string | null; - }>; -} -export interface LoopClosurePortfolioQueue { - listQueue(repoFullName: string | null): Array<{ - status?: unknown; - }>; -} -export interface LoopClosureRunState { - getRunState(repoFullName: string): string | null; -} -export interface LoopClosureSources { - eventLedger: LoopClosureEventLedger; - portfolioQueue: LoopClosurePortfolioQueue; - runState?: LoopClosureRunState; -} -export interface LoopClosureOptions { - /** Event-ledger seq at the END of the prior cycle; events with a strictly greater seq are "this cycle". */ - sinceSeq?: number; - /** Scope the summary to a single repo (its events and queue entries) when set. */ - repoFullName?: string; -} -export interface LoopClosureSummary { - sinceSeq: number | null; - /** Highest event seq observed this cycle (>= sinceSeq); the boundary a caller passes as the next cycle's sinceSeq. */ - lastSeq: number; - events: { - total: number; - byType: Record; - }; - queue: { - total: number; - byStatus: Record; - }; - runState: string | null; -} -/** - * Build a read-only loop-closure summary from local-state sources. Pure: reads `sources` + `options` and returns a - * structured summary, mutating nothing. - */ -export declare function buildLoopClosureSummary(sources: LoopClosureSources, options?: LoopClosureOptions): LoopClosureSummary; diff --git a/packages/loopover-miner/lib/loop-closure.js b/packages/loopover-miner/lib/loop-closure.js deleted file mode 100644 index 2e2a61d3e4..0000000000 --- a/packages/loopover-miner/lib/loop-closure.js +++ /dev/null @@ -1,61 +0,0 @@ -// Loop-closure summary builder (pure, read-only) — #4282, Wave 2 tracker #2353 (miner-manage phase). -// -// A pure, read-only aggregator in the spirit of manage-status.js's collectManageStatus: read across the local-state -// primitives (event ledger, portfolio queue, run-state) and summarize what happened in a completed -// discover→plan→prepare→manage cycle BEFORE the miner loop considers re-entering (idle → discovering again). It -// never calls GitHub, never writes a local store, and never decides whether to re-enter or performs the re-entry -// itself — it only builds the summary a future caller reads before making that call. -// -// Cycle boundary is CALLER-SUPPLIED (deliberately, per the issue): `options.sinceSeq` is the event-ledger seq at the -// END of the prior cycle, so events with a STRICTLY greater seq are "this cycle" — reusing event-ledger.js's own -// `readEvents({ since })` cursor rather than inventing a new persisted cycle-boundary marker. The ledger stores an -// OPEN type vocabulary (only the phase writers define concrete types), so events are tallied GENERICALLY by `type`; -// new phase event types (plans built, PRs prepared/opened, outcomes recorded — landing via sibling issues) surface -// in the tally automatically without a hardcoded list here. -/** - * Build a read-only loop-closure summary from local-state sources. Pure: reads `sources` + `options` and returns a - * structured summary, mutating nothing. - */ -export function buildLoopClosureSummary(sources, options = {}) { - const eventLedger = sources?.eventLedger; - const portfolioQueue = sources?.portfolioQueue; - const runState = sources?.runState; - if (!eventLedger || typeof eventLedger.readEvents !== "function") - throw new Error("invalid_event_ledger"); - if (!portfolioQueue || typeof portfolioQueue.listQueue !== "function") - throw new Error("invalid_portfolio_queue"); - const repoFullName = typeof options.repoFullName === "string" && options.repoFullName.length > 0 ? options.repoFullName : null; - const sinceSeq = Number.isInteger(options.sinceSeq) && options.sinceSeq >= 0 ? options.sinceSeq : null; - // Bound "this cycle" to events after the prior cycle's ending seq; event-ledger applies the `since`/repo filter. - const filter = {}; - if (repoFullName !== null) - filter.repoFullName = repoFullName; - if (sinceSeq !== null) - filter.since = sinceSeq; - const events = eventLedger.readEvents(filter); - const byType = {}; - let lastSeq = sinceSeq ?? 0; - for (const event of events) { - const type = typeof event?.type === "string" && event.type.length > 0 ? event.type : "unknown"; - byType[type] = (byType[type] ?? 0) + 1; - if (Number.isInteger(event?.seq) && event.seq > lastSeq) - lastSeq = event.seq; - } - const byStatus = {}; - const queueEntries = portfolioQueue.listQueue(repoFullName); - for (const entry of queueEntries) { - const status = typeof entry?.status === "string" && entry.status.length > 0 ? entry.status : "unknown"; - byStatus[status] = (byStatus[status] ?? 0) + 1; - } - const currentRunState = runState && typeof runState.getRunState === "function" && repoFullName !== null - ? runState.getRunState(repoFullName) - : null; - return { - sinceSeq, - lastSeq, - events: { total: events.length, byType }, - queue: { total: queueEntries.length, byStatus }, - runState: currentRunState ?? null, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcC1jbG9zdXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9vcC1jbG9zdXJlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHFHQUFxRztBQUNyRyxFQUFFO0FBQ0Ysb0hBQW9IO0FBQ3BILG1HQUFtRztBQUNuRyxnSEFBZ0g7QUFDaEgsaUhBQWlIO0FBQ2pILHFGQUFxRjtBQUNyRixFQUFFO0FBQ0YscUhBQXFIO0FBQ3JILGlIQUFpSDtBQUNqSCxtSEFBbUg7QUFDbkgsb0hBQW9IO0FBQ3BILG1IQUFtSDtBQUNuSCw0REFBNEQ7QUF3QzVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSx1QkFBdUIsQ0FDckMsT0FBMkIsRUFDM0IsVUFBOEIsRUFBRTtJQUVoQyxNQUFNLFdBQVcsR0FBRyxPQUFPLEVBQUUsV0FBVyxDQUFDO0lBQ3pDLE1BQU0sY0FBYyxHQUFHLE9BQU8sRUFBRSxjQUFjLENBQUM7SUFDL0MsTUFBTSxRQUFRLEdBQUcsT0FBTyxFQUFFLFFBQVEsQ0FBQztJQUNuQyxJQUFJLENBQUMsV0FBVyxJQUFJLE9BQU8sV0FBVyxDQUFDLFVBQVUsS0FBSyxVQUFVO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO0lBQzFHLElBQUksQ0FBQyxjQUFjLElBQUksT0FBTyxjQUFjLENBQUMsU0FBUyxLQUFLLFVBQVU7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHlCQUF5QixDQUFDLENBQUM7SUFFbEgsTUFBTSxZQUFZLEdBQ2hCLE9BQU8sT0FBTyxDQUFDLFlBQVksS0FBSyxRQUFRLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDNUcsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUssT0FBTyxDQUFDLFFBQW1CLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFFcEgsaUhBQWlIO0lBQ2pILE1BQU0sTUFBTSxHQUE4QyxFQUFFLENBQUM7SUFDN0QsSUFBSSxZQUFZLEtBQUssSUFBSTtRQUFFLE1BQU0sQ0FBQyxZQUFZLEdBQUcsWUFBWSxDQUFDO0lBQzlELElBQUksUUFBUSxLQUFLLElBQUk7UUFBRSxNQUFNLENBQUMsS0FBSyxHQUFHLFFBQVEsQ0FBQztJQUMvQyxNQUFNLE1BQU0sR0FBRyxXQUFXLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRTlDLE1BQU0sTUFBTSxHQUEyQixFQUFFLENBQUM7SUFDMUMsSUFBSSxPQUFPLEdBQUcsUUFBUSxJQUFJLENBQUMsQ0FBQztJQUM1QixLQUFLLE1BQU0sS0FBSyxJQUFJLE1BQU0sRUFBRSxDQUFDO1FBQzNCLE1BQU0sSUFBSSxHQUFHLE9BQU8sS0FBSyxFQUFFLElBQUksS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLElBQUksQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7UUFDL0YsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUN2QyxJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxJQUFLLEtBQUssQ0FBQyxHQUFjLEdBQUcsT0FBTztZQUFFLE9BQU8sR0FBRyxLQUFLLENBQUMsR0FBYSxDQUFDO0lBQ3JHLENBQUM7SUFFRCxNQUFNLFFBQVEsR0FBMkIsRUFBRSxDQUFDO0lBQzVDLE1BQU0sWUFBWSxHQUFHLGNBQWMsQ0FBQyxTQUFTLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDNUQsS0FBSyxNQUFNLEtBQUssSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUNqQyxNQUFNLE1BQU0sR0FBRyxPQUFPLEtBQUssRUFBRSxNQUFNLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1FBQ3ZHLFFBQVEsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDakQsQ0FBQztJQUVELE1BQU0sZUFBZSxHQUNuQixRQUFRLElBQUksT0FBTyxRQUFRLENBQUMsV0FBVyxLQUFLLFVBQVUsSUFBSSxZQUFZLEtBQUssSUFBSTtRQUM3RSxDQUFDLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQyxZQUFZLENBQUM7UUFDcEMsQ0FBQyxDQUFDLElBQUksQ0FBQztJQUVYLE9BQU87UUFDTCxRQUFRO1FBQ1IsT0FBTztRQUNQLE1BQU0sRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRTtRQUN4QyxLQUFLLEVBQUUsRUFBRSxLQUFLLEVBQUUsWUFBWSxDQUFDLE1BQU0sRUFBRSxRQUFRLEVBQUU7UUFDL0MsUUFBUSxFQUFFLGVBQWUsSUFBSSxJQUFJO0tBQ2xDLENBQUM7QUFDSixDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/loop-reentry.d.ts b/packages/loopover-miner/lib/loop-reentry.d.ts deleted file mode 100644 index 6b894881c0..0000000000 --- a/packages/loopover-miner/lib/loop-reentry.d.ts +++ /dev/null @@ -1,94 +0,0 @@ -export declare const LOOP_REENTRY_DECISION_EVENT: "loop_reentry_decision"; -export type LoopReentryOutcome = "merged" | "disengaged" | "other"; -export type LoopReentryKillSwitchScope = "global" | "repo" | "none"; -export type LoopReentryCandidateInput = { - /** Checked FIRST by the pure `shouldReenter` policy, before any other logic. */ - killSwitchScope: LoopReentryKillSwitchScope; - repoFullName: string; - outcome: LoopReentryOutcome; - maxConsecutiveDisengagements?: number; - maxReentriesPerHour?: number; - maxReentriesPerSession?: number; -}; -export interface LoopReentryEventLedger { - appendEvent(event: { - type: string; - repoFullName?: string; - payload: Record; - }): { - id: number; - seq: number; - type: string; - repoFullName: string | null; - payload: Record; - createdAt: string; - }; - readEvents(filter?: { - since?: number; - repoFullName?: string; - }): Array<{ - type: string; - repoFullName?: string | null; - payload?: Record; - createdAt: string; - }>; -} -export interface LoopReentryPortfolioQueue { - dequeueNext(): { - repoFullName: string; - identifier: string; - priority: number; - status: string; - enqueuedAt: string; - } | null; -} -export interface LoopReentryRunState { - setRunState(repoFullName: string, state: string): unknown; -} -export type LoopReentryDeps = { - eventLedger: LoopReentryEventLedger; - portfolioQueue: LoopReentryPortfolioQueue; - runState?: LoopReentryRunState; - nowMs?: number; - sessionStartMs?: number; - /** The just-completed cycle's read-only summary (loop-closure.js's `buildLoopClosureSummary`), threaded - * through verbatim into the audit event's payload for traceability. Not used to compute the circuit- - * breaker/rate-cap tallies -- see loop-reentry.js's own comment on why. */ - loopSummary?: unknown; -}; -export type LoopReentryResult = { - decision: { - reenter: boolean; - reasons: string[]; - }; - dequeued: { - repoFullName: string; - identifier: string; - priority: number; - status: string; - enqueuedAt: string; - } | null; - event: { - id: number; - seq: number; - type: string; - repoFullName: string | null; - payload: Record; - createdAt: string; - }; -}; -/** - * Count a repo's CONSECUTIVE disengaged (closed-without-merge) PR outcomes, walking backward from the most - * recently recorded PR for that repo until a merged outcome breaks the streak (or history runs out). - */ -export declare function countConsecutiveDisengagements(eventLedger: LoopReentryEventLedger, repoFullName: string): number; -/** Count prior re-entries (successful, i.e. `reentered: true`) recorded at or after `sinceMs`. */ -export declare function countReentriesSince(eventLedger: LoopReentryEventLedger, sinceMs: number): number; -/** - * Evaluate and (if allowed) PERFORM re-entry for one resolved outcome: reads real history to compute the - * circuit-breaker and rate-cap tallies, consults the pure `shouldReenter` policy, and -- only when it allows -- - * dequeues the next candidate and transitions run-state to `"discovering"`. Always appends exactly one audit - * event. Fails closed (throws) on a malformed candidate or missing required dependency, mirroring - * `recordManagePollSnapshot`'s own validation style. - */ -export declare function attemptLoopReentry(candidate: LoopReentryCandidateInput, deps: LoopReentryDeps): LoopReentryResult; diff --git a/packages/loopover-miner/lib/loop-reentry.js b/packages/loopover-miner/lib/loop-reentry.js deleted file mode 100644 index 8314472f6d..0000000000 --- a/packages/loopover-miner/lib/loop-reentry.js +++ /dev/null @@ -1,118 +0,0 @@ -import { shouldReenter } from "@loopover/engine"; -import { readPrOutcomes } from "./pr-outcome.js"; -// Closed-loop discovery re-entry orchestrator (#2338): the real-IO half of "on a resolved outcome (merged, or -// rejected-and-disengaged), automatically re-invoke discovery to select the next candidate." The DECISION -// itself (shouldReenter, @loopover/engine) is pure; this module owns everything that decision -// needs real state for -- reading the repo's own pr_outcome history to compute the per-repo consecutive- -// disengagement tally, reading recent re-entry events for the hourly/session rate cap, and (only when allowed) -// actually dequeuing the next candidate and transitioning run-state. -// -// NOT WIRED INTO ANY AUTOMATIC SCHEDULE: per this issue's own "manual owner sign-off before enabling by -// default in any profile" deliverable, this is a callable function ready for that sign-off -- it is not invoked -// by manage-poll.js or any cron/scheduler as part of this change. -// -// AUDITABILITY: every call appends exactly one `loop_reentry_decision` event to the ledger, whether or not the -// decision allowed re-entry, so the full decision trail (including every suppressed re-entry and why) survives -// independently of this function's own return value. -export const LOOP_REENTRY_DECISION_EVENT = "loop_reentry_decision"; -const HOUR_MS = 60 * 60 * 1000; -/** A `pr_outcome` "closed" decision is this module's practical proxy for "disengaged" -- pr-outcome.js's own - * vocabulary is exactly `"merged" | "closed"` (no separate "disengaged" literal); a PR that closed without - * merging IS the rejected/disengaged case rejection-state-machine.js's own `isRejectedPr` checks for. */ -function isDisengagedOutcome(outcome) { - return outcome?.decision === "closed"; -} -/** - * Count a repo's CONSECUTIVE disengaged (closed-without-merge) PR outcomes, walking backward from the most - * recently recorded PR for that repo until a merged outcome breaks the streak (or history runs out). - */ -export function countConsecutiveDisengagements(eventLedger, repoFullName) { - const outcomes = [...readPrOutcomes(eventLedger, { repoFullName }).values()]; - let count = 0; - for (let i = outcomes.length - 1; i >= 0; i -= 1) { - if (!isDisengagedOutcome(outcomes[i])) - break; - count += 1; - } - return count; -} -/** Count prior re-entries (successful, i.e. `reentered: true`) recorded at or after `sinceMs`. */ -export function countReentriesSince(eventLedger, sinceMs) { - return eventLedger - .readEvents({}) - .filter((event) => event.type === LOOP_REENTRY_DECISION_EVENT && - event.payload?.reentered === true && - Date.parse(event.createdAt) >= sinceMs).length; -} -/** - * Evaluate and (if allowed) PERFORM re-entry for one resolved outcome: reads real history to compute the - * circuit-breaker and rate-cap tallies, consults the pure `shouldReenter` policy, and -- only when it allows -- - * dequeues the next candidate and transitions run-state to `"discovering"`. Always appends exactly one audit - * event. Fails closed (throws) on a malformed candidate or missing required dependency, mirroring - * `recordManagePollSnapshot`'s own validation style. - */ -export function attemptLoopReentry(candidate, deps) { - // Runtime guards retained from the JS (tests may cast malformed inputs past the public types). - if (!candidate || typeof candidate !== "object") - throw new Error("invalid_loop_reentry_candidate"); - if (!["global", "repo", "none"].includes(candidate.killSwitchScope)) - throw new Error("invalid_kill_switch_scope"); - const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : ""; - if (!repoFullName) - throw new Error("invalid_repo_full_name"); - if (!["merged", "disengaged", "other"].includes(candidate.outcome)) - throw new Error("invalid_outcome"); - if (!deps || typeof deps !== "object") - throw new Error("invalid_loop_reentry_deps"); - const { eventLedger, portfolioQueue, runState, nowMs = Date.now(), sessionStartMs = 0 } = deps; - if (!eventLedger || typeof eventLedger.appendEvent !== "function" || typeof eventLedger.readEvents !== "function") { - throw new Error("invalid_event_ledger"); - } - if (!portfolioQueue || typeof portfolioQueue.dequeueNext !== "function") { - throw new Error("invalid_portfolio_queue"); - } - const consecutiveDisengagements = countConsecutiveDisengagements(eventLedger, repoFullName); - const reentriesThisHour = countReentriesSince(eventLedger, nowMs - HOUR_MS); - const reentriesThisSession = countReentriesSince(eventLedger, sessionStartMs); - // Cast: public optional fields omit `| undefined`; engine accepts `number | undefined` under EOPT. - const decision = shouldReenter({ - killSwitchScope: candidate.killSwitchScope, - repoFullName, - outcome: candidate.outcome, - consecutiveDisengagements, - maxConsecutiveDisengagements: candidate.maxConsecutiveDisengagements, - reentriesThisHour, - maxReentriesPerHour: candidate.maxReentriesPerHour, - reentriesThisSession, - maxReentriesPerSession: candidate.maxReentriesPerSession, - }); - let dequeued = null; - if (decision.reenter) { - dequeued = portfolioQueue.dequeueNext(); - if (runState && typeof runState.setRunState === "function") { - runState.setRunState(repoFullName, "discovering"); - } - } - const event = eventLedger.appendEvent({ - type: LOOP_REENTRY_DECISION_EVENT, - repoFullName, - payload: { - killSwitchScope: candidate.killSwitchScope, - outcome: candidate.outcome, - reentered: decision.reenter, - reasons: decision.reasons, - consecutiveDisengagements, - reentriesThisHour, - reentriesThisSession, - dequeuedIdentifier: dequeued ? dequeued.identifier : null, - // The just-completed cycle's read-only summary (loop-closure.js's buildLoopClosureSummary), when the - // caller supplies one -- threaded through verbatim for audit traceability. Optional: the circuit-breaker - // and rate-cap tallies above are computed directly from pr-outcome/event-ledger history (a - // LoopClosureSummary's own byType COUNTS aren't detailed enough to derive a per-repo consecutive- - // disengagement streak from), so this is context, not a computational input. - loopSummary: deps.loopSummary ?? null, - }, - }); - return { decision, dequeued, event }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcC1yZWVudHJ5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9vcC1yZWVudHJ5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUVqRCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFakQsOEdBQThHO0FBQzlHLDBHQUEwRztBQUMxRyw4RkFBOEY7QUFDOUYseUdBQXlHO0FBQ3pHLCtHQUErRztBQUMvRyxxRUFBcUU7QUFDckUsRUFBRTtBQUNGLHdHQUF3RztBQUN4RyxnSEFBZ0g7QUFDaEgsa0VBQWtFO0FBQ2xFLEVBQUU7QUFDRiwrR0FBK0c7QUFDL0csK0dBQStHO0FBQy9HLHFEQUFxRDtBQUVyRCxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBRyx1QkFBZ0MsQ0FBQztBQUM1RSxNQUFNLE9BQU8sR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQWlGL0I7OzBHQUUwRztBQUMxRyxTQUFTLG1CQUFtQixDQUFDLE9BQWtEO0lBQzdFLE9BQU8sT0FBTyxFQUFFLFFBQVEsS0FBSyxRQUFRLENBQUM7QUFDeEMsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSw4QkFBOEIsQ0FDNUMsV0FBbUMsRUFDbkMsWUFBb0I7SUFFcEIsTUFBTSxRQUFRLEdBQUcsQ0FBQyxHQUFHLGNBQWMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFDN0UsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsS0FBSyxJQUFJLENBQUMsR0FBRyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUNqRCxJQUFJLENBQUMsbUJBQW1CLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQUUsTUFBTTtRQUM3QyxLQUFLLElBQUksQ0FBQyxDQUFDO0lBQ2IsQ0FBQztJQUNELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELGtHQUFrRztBQUNsRyxNQUFNLFVBQVUsbUJBQW1CLENBQUMsV0FBbUMsRUFBRSxPQUFlO0lBQ3RGLE9BQU8sV0FBVztTQUNmLFVBQVUsQ0FBQyxFQUFFLENBQUM7U0FDZCxNQUFNLENBQ0wsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUNSLEtBQUssQ0FBQyxJQUFJLEtBQUssMkJBQTJCO1FBQzFDLEtBQUssQ0FBQyxPQUFPLEVBQUUsU0FBUyxLQUFLLElBQUk7UUFDakMsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLElBQUksT0FBTyxDQUN6QyxDQUFDLE1BQU0sQ0FBQztBQUNiLENBQUM7QUFFRDs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUsa0JBQWtCLENBQ2hDLFNBQW9DLEVBQ3BDLElBQXFCO0lBRXJCLCtGQUErRjtJQUMvRixJQUFJLENBQUMsU0FBUyxJQUFJLE9BQU8sU0FBUyxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7SUFDbkcsSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLGVBQWUsQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsMkJBQTJCLENBQUMsQ0FBQztJQUNsSCxNQUFNLFlBQVksR0FBRyxPQUFPLFNBQVMsQ0FBQyxZQUFZLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDckcsSUFBSSxDQUFDLFlBQVk7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDN0QsSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLFlBQVksRUFBRSxPQUFPLENBQUMsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUV2RyxJQUFJLENBQUMsSUFBSSxJQUFJLE9BQU8sSUFBSSxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLDJCQUEyQixDQUFDLENBQUM7SUFDcEYsTUFBTSxFQUFFLFdBQVcsRUFBRSxjQUFjLEVBQUUsUUFBUSxFQUFFLEtBQUssR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEVBQUUsY0FBYyxHQUFHLENBQUMsRUFBRSxHQUFHLElBQUksQ0FBQztJQUMvRixJQUFJLENBQUMsV0FBVyxJQUFJLE9BQU8sV0FBVyxDQUFDLFdBQVcsS0FBSyxVQUFVLElBQUksT0FBTyxXQUFXLENBQUMsVUFBVSxLQUFLLFVBQVUsRUFBRSxDQUFDO1FBQ2xILE1BQU0sSUFBSSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMxQyxDQUFDO0lBQ0QsSUFBSSxDQUFDLGNBQWMsSUFBSSxPQUFPLGNBQWMsQ0FBQyxXQUFXLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDeEUsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFFRCxNQUFNLHlCQUF5QixHQUFHLDhCQUE4QixDQUFDLFdBQVcsRUFBRSxZQUFZLENBQUMsQ0FBQztJQUM1RixNQUFNLGlCQUFpQixHQUFHLG1CQUFtQixDQUFDLFdBQVcsRUFBRSxLQUFLLEdBQUcsT0FBTyxDQUFDLENBQUM7SUFDNUUsTUFBTSxvQkFBb0IsR0FBRyxtQkFBbUIsQ0FBQyxXQUFXLEVBQUUsY0FBYyxDQUFDLENBQUM7SUFFOUUsbUdBQW1HO0lBQ25HLE1BQU0sUUFBUSxHQUFHLGFBQWEsQ0FBQztRQUM3QixlQUFlLEVBQUUsU0FBUyxDQUFDLGVBQWU7UUFDMUMsWUFBWTtRQUNaLE9BQU8sRUFBRSxTQUFTLENBQUMsT0FBTztRQUMxQix5QkFBeUI7UUFDekIsNEJBQTRCLEVBQUUsU0FBUyxDQUFDLDRCQUE0QjtRQUNwRSxpQkFBaUI7UUFDakIsbUJBQW1CLEVBQUUsU0FBUyxDQUFDLG1CQUFtQjtRQUNsRCxvQkFBb0I7UUFDcEIsc0JBQXNCLEVBQUUsU0FBUyxDQUFDLHNCQUFzQjtLQUNsQixDQUFDLENBQUM7SUFFMUMsSUFBSSxRQUFRLEdBQWtDLElBQUksQ0FBQztJQUNuRCxJQUFJLFFBQVEsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNyQixRQUFRLEdBQUcsY0FBYyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ3hDLElBQUksUUFBUSxJQUFJLE9BQU8sUUFBUSxDQUFDLFdBQVcsS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUMzRCxRQUFRLENBQUMsV0FBVyxDQUFDLFlBQVksRUFBRSxhQUFhLENBQUMsQ0FBQztRQUNwRCxDQUFDO0lBQ0gsQ0FBQztJQUVELE1BQU0sS0FBSyxHQUFHLFdBQVcsQ0FBQyxXQUFXLENBQUM7UUFDcEMsSUFBSSxFQUFFLDJCQUEyQjtRQUNqQyxZQUFZO1FBQ1osT0FBTyxFQUFFO1lBQ1AsZUFBZSxFQUFFLFNBQVMsQ0FBQyxlQUFlO1lBQzFDLE9BQU8sRUFBRSxTQUFTLENBQUMsT0FBTztZQUMxQixTQUFTLEVBQUUsUUFBUSxDQUFDLE9BQU87WUFDM0IsT0FBTyxFQUFFLFFBQVEsQ0FBQyxPQUFPO1lBQ3pCLHlCQUF5QjtZQUN6QixpQkFBaUI7WUFDakIsb0JBQW9CO1lBQ3BCLGtCQUFrQixFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsSUFBSTtZQUN6RCxxR0FBcUc7WUFDckcseUdBQXlHO1lBQ3pHLDJGQUEyRjtZQUMzRixrR0FBa0c7WUFDbEcsNkVBQTZFO1lBQzdFLFdBQVcsRUFBRSxJQUFJLENBQUMsV0FBVyxJQUFJLElBQUk7U0FDdEM7S0FDRixDQUFDLENBQUM7SUFFSCxPQUFPLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsQ0FBQztBQUN2QyxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/manage-poll.d.ts b/packages/loopover-miner/lib/manage-poll.d.ts deleted file mode 100644 index fbbbcf0369..0000000000 --- a/packages/loopover-miner/lib/manage-poll.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { PollCheckRunsOptions, PollCheckRunsResult } from "./ci-poller.js"; -import type { EventLedger, LedgerEntry } from "./event-ledger.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -export type ManagePollInput = { - repoFullName: string; - prNumber: number; - branch?: string | null; -}; -export type ManagePollEventPayload = { - prNumber: number; - branch: string | null; - ciState: PollCheckRunsResult["conclusion"]; - gateVerdict: string; - outcome: string; - lastPolledAt: string; -}; -export type ManagePollRecordResult = { - pollResult: PollCheckRunsResult; - payload: ManagePollEventPayload; - event: LedgerEntry; -}; -export type ParsedManagePollArgs = { - repoFullName: string; - prNumber: number; - branch: string | null; - dryRun: boolean; - json: boolean; -} | { - error: string; -}; -export declare function mapPollConclusionToGateVerdict(conclusion: PollCheckRunsResult["conclusion"]): string; -export declare function mapPollConclusionToOutcome(conclusion: PollCheckRunsResult["conclusion"]): string; -export declare function buildManagePollEventPayload(prNumber: number, pollResult: PollCheckRunsResult, options?: { - branch?: string | null; - lastPolledAt?: string; -}): ManagePollEventPayload; -export declare function parseManagePollArgs(args?: string[]): ParsedManagePollArgs; -/** - * Poll GitHub check runs for a managed PR and append a `manage_pr_update` snapshot to the local event ledger. - * Completes the manage-status data path introduced in #2325 / #3070 using the CI poller from #2323. - */ -export declare function recordManagePollSnapshot(input: ManagePollInput, options?: { - eventLedger: EventLedger; - portfolioQueue?: PortfolioQueueStore; - ensurePortfolioRow?: boolean; - pollCheckRuns?: (repoFullName: string, prNumber: number, options?: PollCheckRunsOptions) => Promise; - lastPolledAt?: string; -} & PollCheckRunsOptions): Promise; -export declare function runManagePoll(args?: string[], options?: { - initEventLedger?: () => EventLedger; - initPortfolioQueue?: () => PortfolioQueueStore; - ensurePortfolioRow?: boolean; - pollCheckRuns?: (repoFullName: string, prNumber: number, options?: PollCheckRunsOptions) => Promise; - githubToken?: string; - lastPolledAt?: string; -} & PollCheckRunsOptions): Promise; diff --git a/packages/loopover-miner/lib/manage-poll.js b/packages/loopover-miner/lib/manage-poll.js deleted file mode 100644 index e53924342e..0000000000 --- a/packages/loopover-miner/lib/manage-poll.js +++ /dev/null @@ -1,252 +0,0 @@ -import { pollCheckRuns } from "./ci-poller.js"; -import { initEventLedger } from "./event-ledger.js"; -import { MANAGE_PR_UPDATE_EVENT, formatManagedPrIdentifier, } from "./manage-status.js"; -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { resolveGitHubToken } from "./github-token-resolution.js"; -const MANAGE_POLL_USAGE = "Usage: loopover-miner manage poll [--branch ] [--dry-run] [--json]"; -// `value` is always a real string here: this function is private and only ever called with `positional[0]` -// immediately after the `positional.length !== 2` guard in parseManagePollArgs, which already proves it defined. -function parseRepoArg(value) { - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -export function mapPollConclusionToGateVerdict(conclusion) { - switch (conclusion) { - case "success": - return "pass"; - case "failure": - return "block"; - default: - return "advisory"; - } -} -export function mapPollConclusionToOutcome(conclusion) { - switch (conclusion) { - case "success": - return "ready"; - case "failure": - return "needs-work"; - default: - return "open"; - } -} -export function buildManagePollEventPayload(prNumber, pollResult, options = {}) { - if (!Number.isInteger(prNumber) || prNumber <= 0) - throw new Error("invalid_pr_number"); - if (!pollResult || typeof pollResult !== "object") - throw new Error("invalid_poll_result"); - const branch = typeof options.branch === "string" && options.branch.trim() ? options.branch.trim() : null; - const lastPolledAt = typeof options.lastPolledAt === "string" && options.lastPolledAt.trim() - ? options.lastPolledAt.trim() - : new Date().toISOString(); - return { - prNumber, - branch, - ciState: pollResult.conclusion, - gateVerdict: mapPollConclusionToGateVerdict(pollResult.conclusion), - outcome: mapPollConclusionToOutcome(pollResult.conclusion), - lastPolledAt, - }; -} -export function parseManagePollArgs(args = []) { - const options = { json: false, branch: null, dryRun: false }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: still runs the real (read-only) CI-check-run poll, but skips the event-ledger append and - // portfolio-queue enqueue. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--branch") { - const branch = args[index + 1]; - if (!branch || branch.startsWith("-")) - return { error: MANAGE_POLL_USAGE }; - options.branch = branch; - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - positional.push(token); - } - if (positional.length !== 2) - return { error: MANAGE_POLL_USAGE }; - const repo = parseRepoArg(positional[0]); - if ("error" in repo) - return repo; - const prNumber = Number(positional[1]); - if (!Number.isInteger(prNumber) || prNumber <= 0) { - return { error: "Pull request number must be a positive integer." }; - } - return { - repoFullName: repo.repoFullName, - prNumber, - ...options, - }; -} -/** The forge host a managed-PR row belongs to. Mirrors portfolio-queue-manager.js's own fold (and every - * store's `normalizeApiBaseUrl`): omitted/blank → the github.com default, so a single-forge caller is - * unaffected. Used only to COMPARE hosts here; `enqueue` still does its own normalization/validation. */ -function resolveManagedRowApiBaseUrl(apiBaseUrl) { - return typeof apiBaseUrl === "string" && apiBaseUrl.trim() ? apiBaseUrl.trim() : DEFAULT_FORGE_CONFIG.apiBaseUrl; -} -function ensureManagedPrRow(portfolioQueue, repoFullName, prNumber, apiBaseUrl) { - const identifier = formatManagedPrIdentifier(prNumber); - // `listQueue(repoFullName)` is forge-BLIND, so the existence check has to compare the host too: the queue's - // composite (api_base_url, repo_full_name, identifier) key exists precisely so two hosts serving the same - // owner/repo name never collide (#5563). Without this scoping, the same repo+PR-number already tracked on - // ANOTHER host suppresses this host's row entirely. - const targetApiBaseUrl = resolveManagedRowApiBaseUrl(apiBaseUrl); - const exists = portfolioQueue - .listQueue(repoFullName) - .some((entry) => entry.identifier === identifier && resolveManagedRowApiBaseUrl(entry.apiBaseUrl) === targetApiBaseUrl); - if (!exists) { - // Thread the SAME apiBaseUrl the CI poll above used, so the row is scoped to the host it was polled from - // instead of silently defaulting to github.com. - portfolioQueue.enqueue({ repoFullName, identifier, priority: 0, ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}) }); - } -} -/** - * Poll GitHub check runs for a managed PR and append a `manage_pr_update` snapshot to the local event ledger. - * Completes the manage-status data path introduced in #2325 / #3070 using the CI poller from #2323. - */ -export async function recordManagePollSnapshot(input, options = {}) { - if (!input || typeof input !== "object") - throw new Error("invalid_manage_poll_input"); - const repoFullName = typeof input.repoFullName === "string" ? input.repoFullName.trim() : ""; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - if (!Number.isInteger(input.prNumber) || input.prNumber <= 0) - throw new Error("invalid_pr_number"); - const eventLedger = options.eventLedger; - if (!eventLedger || typeof eventLedger.appendEvent !== "function") { - throw new Error("invalid_event_ledger"); - } - const portfolioQueue = options.portfolioQueue; - if (options.portfolioQueue !== undefined) { - if (!portfolioQueue || typeof portfolioQueue.enqueue !== "function") { - throw new Error("invalid_portfolio_queue"); - } - } - const pollCheckRunsFn = options.pollCheckRuns ?? pollCheckRuns; - // PollCheckRunsOptions's fields are all optional under exactOptionalPropertyTypes (no explicit `| undefined`), - // but every field here is read downstream via a plain `??`/truthiness check, so passing an explicit - // `undefined` through behaves identically to omitting the key -- the cast just skips constructing a - // conditional per field for a distinction nothing downstream can observe. - const pollResult = await pollCheckRunsFn(repoFullName, input.prNumber, { - apiBaseUrl: options.apiBaseUrl, - fetchFn: options.fetchFn, - githubToken: options.githubToken ?? "", - maxAttempts: options.maxAttempts, - minIntervalMs: options.minIntervalMs, - maxIntervalMs: options.maxIntervalMs, - sleepFn: options.sleepFn, - }); - const payload = buildManagePollEventPayload(input.prNumber, pollResult, { - branch: input.branch, - lastPolledAt: options.lastPolledAt, - }); - if ((options.ensurePortfolioRow ?? true) && portfolioQueue) { - ensureManagedPrRow(portfolioQueue, repoFullName, input.prNumber, options.apiBaseUrl); - } - const event = eventLedger.appendEvent({ - type: MANAGE_PR_UPDATE_EVENT, - repoFullName, - payload, - }); - return { pollResult, payload, event }; -} -export async function runManagePoll(args = [], options = {}) { - const parsed = parseManagePollArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - // #4847: the CI-check-run poll itself is a real, read-only GitHub signal -- the useful "what would this - // record?" output -- so a dry run still performs it for real. It never opens the event ledger or portfolio - // queue, though: a no-op event ledger is fed through recordManagePollSnapshot so its own real payload-building - // logic still runs, just without ever writing to local storage (ensurePortfolioRow: false skips the queue - // enqueue the same way). - if (parsed.dryRun) { - const noopEventLedger = { appendEvent: () => null }; - try { - const result = await recordManagePollSnapshot({ repoFullName: parsed.repoFullName, prNumber: parsed.prNumber, branch: parsed.branch }, { - eventLedger: noopEventLedger, - ensurePortfolioRow: false, - pollCheckRuns: options.pollCheckRuns, - fetchFn: options.fetchFn, - githubToken: options.githubToken ?? (await resolveGitHubToken(process.env)) ?? "", - apiBaseUrl: options.apiBaseUrl, - maxAttempts: options.maxAttempts, - minIntervalMs: options.minIntervalMs, - maxIntervalMs: options.maxIntervalMs, - sleepFn: options.sleepFn, - lastPolledAt: options.lastPolledAt, - }); - const dryRunResult = { outcome: "dry_run", pollResult: result.pollResult, payload: result.payload }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: ${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome}). No event-ledger or portfolio-queue write was made.`); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - } - const ownsEventLedger = options.initEventLedger === undefined; - const ownsPortfolioQueue = options.initPortfolioQueue === undefined; - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - try { - const result = await recordManagePollSnapshot({ - repoFullName: parsed.repoFullName, - prNumber: parsed.prNumber, - branch: parsed.branch, - }, { - eventLedger, - portfolioQueue, - ensurePortfolioRow: options.ensurePortfolioRow ?? true, - pollCheckRuns: options.pollCheckRuns, - fetchFn: options.fetchFn, - githubToken: options.githubToken ?? (await resolveGitHubToken(process.env)) ?? "", - apiBaseUrl: options.apiBaseUrl, - maxAttempts: options.maxAttempts, - minIntervalMs: options.minIntervalMs, - maxIntervalMs: options.maxIntervalMs, - sleepFn: options.sleepFn, - lastPolledAt: options.lastPolledAt, - }); - if (parsed.json) { - console.log(JSON.stringify(result, null, 2)); - } - else { - console.log(`${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome})`); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - if (ownsEventLedger) - eventLedger.close(); - if (ownsPortfolioQueue) - portfolioQueue.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlLXBvbGwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtYW5hZ2UtcG9sbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFL0MsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXBELE9BQU8sRUFDTCxzQkFBc0IsRUFDdEIseUJBQXlCLEdBQzFCLE1BQU0sb0JBQW9CLENBQUM7QUFDNUIsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFFL0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDekQsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2xGLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRWxFLE1BQU0saUJBQWlCLEdBQ3JCLDZGQUE2RixDQUFDO0FBaUNoRywyR0FBMkc7QUFDM0csaUhBQWlIO0FBQ2pILFNBQVMsWUFBWSxDQUFDLEtBQWE7SUFDakMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzdCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDM0MsT0FBTyxFQUFFLEtBQUssRUFBRSx3Q0FBd0MsRUFBRSxDQUFDO0lBQzdELENBQUM7SUFDRCxPQUFPLEVBQUUsWUFBWSxFQUFFLEdBQUcsS0FBSyxJQUFJLElBQUksRUFBRSxFQUFFLENBQUM7QUFDOUMsQ0FBQztBQUVELE1BQU0sVUFBVSw4QkFBOEIsQ0FBQyxVQUE2QztJQUMx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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/manage-status.d.ts b/packages/loopover-miner/lib/manage-status.d.ts deleted file mode 100644 index 2694068c98..0000000000 --- a/packages/loopover-miner/lib/manage-status.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { EventLedger, LedgerEntry } from "./event-ledger.js"; -import type { PortfolioQueueStore, QueueStatus } from "./portfolio-queue.js"; -import type { RunState, RunStateStore } from "./run-state.js"; -/** Event vocabulary for manage-phase PR snapshots written by manage poll. (#2325) */ -export declare const MANAGE_PR_UPDATE_EVENT = "manage_pr_update"; -export declare const MANAGED_PR_IDENTIFIER_PREFIX = "pr:"; -export type ManageStatusRow = { - repoFullName: string; - prNumber: number; - branch: string | null; - ciState: string | null; - gateVerdict: string | null; - outcome: string | null; - lastPolledAt: string | null; - queueStatus: QueueStatus | null; - priority: number | null; -}; -export type ManageStatusSources = { - portfolioQueue: PortfolioQueueStore; - eventLedger: EventLedger; -}; -export type RunPortfolioSources = ManageStatusSources & { - runStateStore: RunStateStore; -}; -export type RunPortfolioRow = { - repoFullName: string; - runState: RunState | null; - runStateUpdatedAt: string | null; - prCount: number; - prs: ManageStatusRow[]; -}; -export type ManageUpdateSnapshot = { - repoFullName: string; - prNumber: number; - branch: string | null; - ciState: string | null; - gateVerdict: string | null; - outcome: string | null; - lastPolledAt: string | null; -}; -export declare function parseManagedPrIdentifier(identifier: unknown): number | null; -export declare function formatManagedPrIdentifier(prNumber: number): string; -/** Index the latest manage snapshot per repo/PR from ascending ledger events. Pure. */ -export declare function indexLatestManageUpdates(events: LedgerEntry[]): Map; -/** - * Aggregate managed PR rows from the local portfolio queue and append-only event ledger. Read-only — never calls - * GitHub or mutates local stores. (#2325) - */ -export declare function collectManageStatus(sources: ManageStatusSources): ManageStatusRow[]; -/** - * Fold each tracked repo's current discover/plan/prepare run state alongside its managed PR rows into one - * "run portfolio" row per repo (#4279). `collectManageStatus` alone is PR-scoped only and never surfaces the - * run-state signal, so a repo actively discovering/planning with zero PRs yet is otherwise invisible. A repo - * appears here if it has EITHER a recorded run state OR at least one managed PR row. - */ -export declare function collectRunPortfolio(sources: RunPortfolioSources): RunPortfolioRow[]; -export declare function renderManageStatusTable(rows: ManageStatusRow[]): string; -/** One row per tracked repo (run state + PR count), the compact companion to {@link renderManageStatusTable}'s - * per-PR detail (#4279). */ -export declare function renderRunPortfolioTable(portfolio: RunPortfolioRow[]): string; -export declare function parseManageStatusArgs(args?: string[]): { - json: boolean; -} | { - error: string; -}; -export declare function runManageStatus(args?: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; - initEventLedger?: () => EventLedger; - initRunStateStore?: () => RunStateStore; -}): number; diff --git a/packages/loopover-miner/lib/manage-status.js b/packages/loopover-miner/lib/manage-status.js deleted file mode 100644 index 9a58a3c731..0000000000 --- a/packages/loopover-miner/lib/manage-status.js +++ /dev/null @@ -1,248 +0,0 @@ -import { initEventLedger } from "./event-ledger.js"; -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { initRunStateStore } from "./run-state.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -/** Event vocabulary for manage-phase PR snapshots written by manage poll. (#2325) */ -export const MANAGE_PR_UPDATE_EVENT = "manage_pr_update"; -export const MANAGED_PR_IDENTIFIER_PREFIX = "pr:"; -export function parseManagedPrIdentifier(identifier) { - if (typeof identifier !== "string") - return null; - const match = identifier.match(/^pr:(\d+)$/); - if (!match) - return null; - const prNumber = Number(match[1]); - return Number.isInteger(prNumber) && prNumber > 0 ? prNumber : null; -} -export function formatManagedPrIdentifier(prNumber) { - if (!Number.isInteger(prNumber) || prNumber <= 0) - throw new Error("invalid_pr_number"); - return `${MANAGED_PR_IDENTIFIER_PREFIX}${prNumber}`; -} -function optionalString(value) { - if (value === undefined || value === null) - return null; - if (typeof value !== "string") - return null; - const trimmed = value.trim(); - return trimmed || null; -} -function normalizeManageUpdatePayload(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return null; - const input = payload; - if (!Number.isInteger(input.prNumber) || input.prNumber <= 0) - return null; - return { - prNumber: input.prNumber, - branch: optionalString(input.branch), - ciState: optionalString(input.ciState), - gateVerdict: optionalString(input.gateVerdict), - outcome: optionalString(input.outcome), - lastPolledAt: optionalString(input.lastPolledAt), - }; -} -/** Index the latest manage snapshot per repo/PR from ascending ledger events. Pure. */ -export function indexLatestManageUpdates(events) { - const latest = new Map(); - for (const event of Array.isArray(events) ? events : []) { - if (event?.type !== MANAGE_PR_UPDATE_EVENT) - continue; - if (typeof event.repoFullName !== "string" || !event.repoFullName.trim()) - continue; - const normalized = normalizeManageUpdatePayload(event.payload); - if (!normalized) - continue; - const key = `${event.repoFullName}:${normalized.prNumber}`; - latest.set(key, { ...normalized, repoFullName: event.repoFullName }); - } - return latest; -} -/** - * Aggregate managed PR rows from the local portfolio queue and append-only event ledger. Read-only — never calls - * GitHub or mutates local stores. (#2325) - */ -export function collectManageStatus(sources) { - const portfolioQueue = sources?.portfolioQueue; - const eventLedger = sources?.eventLedger; - if (!portfolioQueue || typeof portfolioQueue.listQueue !== "function") { - throw new Error("invalid_portfolio_queue"); - } - if (!eventLedger || typeof eventLedger.readEvents !== "function") { - throw new Error("invalid_event_ledger"); - } - const rowsByKey = new Map(); - for (const entry of portfolioQueue.listQueue(null)) { - const prNumber = parseManagedPrIdentifier(entry.identifier); - if (prNumber === null) - continue; - const key = `${entry.repoFullName}:${prNumber}`; - rowsByKey.set(key, { - repoFullName: entry.repoFullName, - prNumber, - branch: null, - ciState: null, - gateVerdict: null, - outcome: null, - lastPolledAt: null, - queueStatus: entry.status, - priority: entry.priority, - }); - } - for (const [key, update] of indexLatestManageUpdates(eventLedger.readEvents())) { - const existing = rowsByKey.get(key); - rowsByKey.set(key, { - repoFullName: update.repoFullName, - prNumber: update.prNumber, - branch: update.branch, - ciState: update.ciState, - gateVerdict: update.gateVerdict, - outcome: update.outcome, - lastPolledAt: update.lastPolledAt, - queueStatus: existing?.queueStatus ?? null, - priority: existing?.priority ?? null, - }); - } - return [...rowsByKey.values()].sort((left, right) => { - const repoCmp = left.repoFullName.localeCompare(right.repoFullName); - if (repoCmp !== 0) - return repoCmp; - return left.prNumber - right.prNumber; - }); -} -/** - * Fold each tracked repo's current discover/plan/prepare run state alongside its managed PR rows into one - * "run portfolio" row per repo (#4279). `collectManageStatus` alone is PR-scoped only and never surfaces the - * run-state signal, so a repo actively discovering/planning with zero PRs yet is otherwise invisible. A repo - * appears here if it has EITHER a recorded run state OR at least one managed PR row. - */ -export function collectRunPortfolio(sources) { - const runStateStore = sources?.runStateStore; - if (!runStateStore || typeof runStateStore.listRunStates !== "function") { - throw new Error("invalid_run_state_store"); - } - const prsByRepo = new Map(); - for (const row of collectManageStatus(sources)) { - const list = prsByRepo.get(row.repoFullName) ?? []; - list.push(row); - prsByRepo.set(row.repoFullName, list); - } - // NOTE (#5563): keyed by repoFullName alone, not apiBaseUrl -- this dashboard fold predates multi-forge run - // states and produces exactly ONE row per repo name. If the same repo name has a recorded run state on two - // different hosts, only one (the later entry in listRunStates' order) survives here; the other's row is still - // intact in the store, just not surfaced in this particular view. Safe (no data loss, no write), just a display - // limitation -- broadening this fold to be host-aware is a separate, larger dashboard-shape change. - const runStateByRepo = new Map(runStateStore.listRunStates().map((entry) => [entry.repoFullName, entry])); - const repoFullNames = new Set([...prsByRepo.keys(), ...runStateByRepo.keys()]); - return [...repoFullNames].sort((left, right) => left.localeCompare(right)).map((repoFullName) => { - const prs = prsByRepo.get(repoFullName) ?? []; - const runState = runStateByRepo.get(repoFullName); - return { - repoFullName, - runState: runState?.state ?? null, - runStateUpdatedAt: runState?.updatedAt ?? null, - prCount: prs.length, - prs, - }; - }); -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderManageStatusTable(rows) { - if (!Array.isArray(rows) || rows.length === 0) - return "no managed pull requests"; - const header = [ - "repo".padEnd(24), - "pr".padStart(4), - "branch".padEnd(16), - "ci".padEnd(10), - "gate".padEnd(10), - "outcome".padEnd(10), - "last-polled".padEnd(20), - "queue".padEnd(12), - "pri".padStart(4), - ].join(" "); - const lines = rows.map((row) => [ - row.repoFullName.padEnd(24), - String(row.prNumber).padStart(4), - display(row.branch).padEnd(16), - display(row.ciState).padEnd(10), - display(row.gateVerdict).padEnd(10), - display(row.outcome).padEnd(10), - display(row.lastPolledAt).padEnd(20), - display(row.queueStatus).padEnd(12), - display(row.priority).padStart(4), - ].join(" ")); - return [header, ...lines].join("\n"); -} -/** One row per tracked repo (run state + PR count), the compact companion to {@link renderManageStatusTable}'s - * per-PR detail (#4279). */ -export function renderRunPortfolioTable(portfolio) { - if (!Array.isArray(portfolio) || portfolio.length === 0) - return "no tracked repos"; - const header = [ - "repo".padEnd(24), - "run-state".padEnd(12), - "updated".padEnd(20), - "prs".padStart(4), - ].join(" "); - const lines = portfolio.map((entry) => [ - entry.repoFullName.padEnd(24), - display(entry.runState).padEnd(12), - display(entry.runStateUpdatedAt).padEnd(20), - String(entry.prCount).padStart(4), - ].join(" ")); - return [header, ...lines].join("\n"); -} -export function parseManageStatusArgs(args = []) { - for (const token of args) { - if (token === "--json") - continue; - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - return { error: "Usage: loopover-miner manage status [--json]" }; - } - return { json: args.includes("--json") }; -} -export function runManageStatus(args = [], options = {}) { - const parsed = parseManageStatusArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const ownsPortfolioQueue = options.initPortfolioQueue === undefined; - const ownsEventLedger = options.initEventLedger === undefined; - const ownsRunStateStore = options.initRunStateStore === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - const runStateStore = (options.initRunStateStore ?? initRunStateStore)(); - try { - const rows = collectManageStatus({ portfolioQueue, eventLedger }); - const runPortfolio = collectRunPortfolio({ portfolioQueue, eventLedger, runStateStore }); - if (parsed.json) { - // Additive only (#4279): `rows` keeps its existing shape unchanged; `runPortfolio` is a new key so an - // existing consumer parsing this JSON for `rows` alone sees byte-identical output. - console.log(JSON.stringify({ rows, runPortfolio }, null, 2)); - } - else { - console.log(`${renderManageStatusTable(rows)}\n\n${renderRunPortfolioTable(runPortfolio)}`); - } - return 0; - } - catch (error) { - // Collecting/rendering manage status touches three SQLite stores; a read/render failure must surface as a - // clean CLI error (honoring --json), not an unhandled throw -- matching runOrbExportCli / runQueueList (#7236). - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - if (ownsPortfolioQueue) - portfolioQueue.close(); - if (ownsEventLedger) - eventLedger.close(); - if (ownsRunStateStore) - runStateStore.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlLXN0YXR1cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIm1hbmFnZS1zdGF0dXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXBELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBRS9ELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRW5ELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRixxRkFBcUY7QUFDckYsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsa0JBQWtCLENBQUM7QUFDekQsTUFBTSxDQUFDLE1BQU0sNEJBQTRCLEdBQUcsS0FBSyxDQUFDO0FBa0RsRCxNQUFNLFVBQVUsd0JBQXdCLENBQUMsVUFBbUI7SUFDMUQsSUFBSSxPQUFPLFVBQVUsS0FBSyxRQUFRO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDaEQsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQztJQUM3QyxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3hCLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNsQyxPQUFPLE1BQU0sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7QUFDdEUsQ0FBQztBQUVELE1BQU0sVUFBVSx5QkFBeUIsQ0FBQyxRQUFnQjtJQUN4RCxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsSUFBSSxRQUFRLElBQUksQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsbUJBQW1C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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/metrics-cli.d.ts b/packages/loopover-miner/lib/metrics-cli.d.ts deleted file mode 100644 index 31bb34d447..0000000000 --- a/packages/loopover-miner/lib/metrics-cli.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MinerPredictionMetricRow } from "@loopover/engine"; -import type { PredictionLedger } from "./prediction-ledger.js"; -/** - * Project prediction-ledger rows onto the engine renderer's metric-row shape -- the predicted `conclusion` only. - * The realized-outcome pairing (`correct`) is intentionally left unset: the miner has no outcome-join yet, so the - * correct/incorrect counters stay zero and only `predictions_total{conclusion}` moves -- exactly how the renderer - * is designed to degrade before outcome-pairing exists (see its header comment). - */ -export declare function collectPredictionMetricRows(ledger: PredictionLedger): MinerPredictionMetricRow[]; -export declare function runMetrics(args: string[], options?: { - initPredictionLedger?: () => PredictionLedger; -}): number; diff --git a/packages/loopover-miner/lib/metrics-cli.js b/packages/loopover-miner/lib/metrics-cli.js deleted file mode 100644 index 0aec1886ac..0000000000 --- a/packages/loopover-miner/lib/metrics-cli.js +++ /dev/null @@ -1,48 +0,0 @@ -import { renderMinerPredictionMetrics } from "@loopover/engine"; -import { initPredictionLedger } from "./prediction-ledger.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -// `metrics` (#4838): render the miner's prediction-calibration counters as Prometheus text-exposition to stdout, -// for a scrape wrapper or cron redirect. The counters are produced by the engine's already-built -// renderMinerPredictionMetrics (packages/loopover-engine/src/miner-prediction-metrics.ts) -- this command only -// reads the local prediction ledger and feeds it in, never touching the renderer itself. Strictly local + offline: -// no network, no writes. -const METRICS_USAGE = "Usage: loopover-miner metrics"; -/** - * Project prediction-ledger rows onto the engine renderer's metric-row shape -- the predicted `conclusion` only. - * The realized-outcome pairing (`correct`) is intentionally left unset: the miner has no outcome-join yet, so the - * correct/incorrect counters stay zero and only `predictions_total{conclusion}` moves -- exactly how the renderer - * is designed to degrade before outcome-pairing exists (see its header comment). - */ -export function collectPredictionMetricRows(ledger) { - return ledger.readPredictions().map((entry) => ({ conclusion: entry.conclusion })); -} -// Open the local prediction ledger (or a test-injected one) for the duration of `run`, closing it only when we -// opened it -- an injected ledger is owned by the caller. Mirrors event-ledger-cli.js's withEventLedger. -function withPredictionLedger(options, run) { - const ownsLedger = options.initPredictionLedger === undefined; - const ledger = (options.initPredictionLedger ?? initPredictionLedger)(); - try { - return run(ledger); - } - finally { - if (ownsLedger) - ledger.close(); - } -} -export function runMetrics(args, options = {}) { - if (args.length > 0) { - return reportCliFailure(argsWantJson(args), METRICS_USAGE); - } - try { - return withPredictionLedger(options, (ledger) => { - // renderMinerPredictionMetrics returns a newline-terminated document; console.log re-adds the terminator, so - // trim it to emit exactly one trailing newline. - console.log(renderMinerPredictionMetrics(collectPredictionMetricRows(ledger)).trimEnd()); - return 0; - }); - } - catch (error) { - return reportCliFailure(argsWantJson(args), describeCliError(error)); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtZXRyaWNzLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUVoRSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUU5RCxPQUFPLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFbEYsaUhBQWlIO0FBQ2pILGlHQUFpRztBQUNqRywrR0FBK0c7QUFDL0csbUhBQW1IO0FBQ25ILHlCQUF5QjtBQUV6QixNQUFNLGFBQWEsR0FBRywrQkFBK0IsQ0FBQztBQUV0RDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSwyQkFBMkIsQ0FBQyxNQUF3QjtJQUNsRSxPQUFPLE1BQU0sQ0FBQyxlQUFlLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsS0FBSyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUNyRixDQUFDO0FBRUQsK0dBQStHO0FBQy9HLHlHQUF5RztBQUN6RyxTQUFTLG9CQUFvQixDQUMzQixPQUEwRCxFQUMxRCxHQUFvQztJQUVwQyxNQUFNLFVBQVUsR0FBRyxPQUFPLENBQUMsb0JBQW9CLEtBQUssU0FBUyxDQUFDO0lBQzlELE1BQU0sTUFBTSxHQUFHLENBQUMsT0FBTyxDQUFDLG9CQUFvQixJQUFJLG9CQUFvQixDQUFDLEVBQUUsQ0FBQztJQUN4RSxJQUFJLENBQUM7UUFDSCxPQUFPLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUNyQixDQUFDO1lBQVMsQ0FBQztRQUNULElBQUksVUFBVTtZQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUNqQyxDQUFDO0FBQ0gsQ0FBQztBQUVELE1BQU0sVUFBVSxVQUFVLENBQUMsSUFBYyxFQUFFLFVBQTZELEVBQUU7SUFDeEcsSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3BCLE9BQU8sZ0JBQWdCLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxFQUFFLGFBQWEsQ0FBQyxDQUFDO0lBQzdELENBQUM7SUFFRCxJQUFJLENBQUM7UUFDSCxPQUFPLG9CQUFvQixDQUFDLE9BQU8sRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFO1lBQzlDLDZHQUE2RztZQUM3RyxnREFBZ0Q7WUFDaEQsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0QkFBNEIsQ0FBQywyQkFBMkIsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDekYsT0FBTyxDQUFDLENBQUM7UUFDWCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsT0FBTyxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEVBQUUsZ0JBQWdCLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztJQUN2RSxDQUFDO0FBQ0gsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/migrate-cli.d.ts b/packages/loopover-miner/lib/migrate-cli.d.ts deleted file mode 100644 index 03b2de58f6..0000000000 --- a/packages/loopover-miner/lib/migrate-cli.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type MigrateStatus = "skipped" | "up-to-date" | "migrated" | "failed"; -export type MigrateResult = { - name: string; - dbPath: string; - ok: boolean; - status: MigrateStatus; - detail: string; - versionBefore: number | null; - versionAfter: number | null; -}; -export type MigrateStoreDescriptor = { - name: string; - resolveDbPath: (env?: Record) => string; - open: (dbPath: string) => { - close: () => void; - }; -}; -/** `stores` is injectable so tests can exercise a store descriptor's failure paths (e.g. a non-Error throw) - * without depending on real node:sqlite error shapes; defaults to the real seven-store list. */ -export declare function runMigrateChecks(env?: Record, stores?: MigrateStoreDescriptor[]): MigrateResult[]; -export declare function runMigrate(args?: string[], env?: Record): number; diff --git a/packages/loopover-miner/lib/migrate-cli.js b/packages/loopover-miner/lib/migrate-cli.js deleted file mode 100644 index 96b189e4a1..0000000000 --- a/packages/loopover-miner/lib/migrate-cli.js +++ /dev/null @@ -1,161 +0,0 @@ -// Proactive schema-migration runner for the miner's local SQLite stores (#4871). Every store already applies -// its own pending migrations (schema-version.js's applySchemaMigrations) as a side effect of being opened by -// whatever command happens to touch it first -- this command instead lets an operator PROACTIVELY bring every -// known store's EXISTING on-disk file up to date in one pass (e.g. right after upgrading, or before starting a -// fleet), without needing to guess which command happens to touch which store first. Mirrors status.js's -// storeIntegrityChecks [name, resolve*DbPath(env)] store list exactly (same eleven stores `doctor` already -// covers, #6768), but actually OPENS each store (rather than a read-only integrity probe) so its real open/init -// function's migration path runs for real. A store file that does not exist yet is skipped, not created -- -// "migrate" brings existing files up to date; it is not another way to bootstrap fresh state (that's `init`). -import { existsSync } from "node:fs"; -import { DatabaseSync } from "node:sqlite"; -import { readSchemaVersion } from "./schema-version.js"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js"; -import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js"; -import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js"; -import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js"; -import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; -import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js"; -import { openPlanStore, resolvePlanStoreDbPath } from "./plan-store.js"; -import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; -import { initAttemptLog, resolveAttemptLogDbPath } from "./attempt-log.js"; -import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; -import { openWorktreeAllocator, resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; -import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; -import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; -import { initPolicyDocCacheStore, resolvePolicyDocCacheDbPath } from "./policy-doc-cache.js"; -const MIGRATE_USAGE = "Usage: loopover-miner migrate [--json]"; -const STORES = [ - { name: "event-ledger", resolveDbPath: resolveEventLedgerDbPath, open: initEventLedger }, - { name: "governor-ledger", resolveDbPath: resolveGovernorLedgerDbPath, open: initGovernorLedger }, - { name: "prediction-ledger", resolveDbPath: resolvePredictionLedgerDbPath, open: initPredictionLedger }, - { name: "portfolio-queue", resolveDbPath: resolvePortfolioQueueDbPath, open: initPortfolioQueueStore }, - { name: "claim-ledger", resolveDbPath: resolveClaimLedgerDbPath, open: openClaimLedger }, - { name: "run-state", resolveDbPath: resolveRunStateDbPath, open: initRunStateStore }, - { name: "plan-store", resolveDbPath: resolvePlanStoreDbPath, open: openPlanStore }, - { name: "governor-state", resolveDbPath: resolveGovernorStateDbPath, open: openGovernorState }, - { name: "attempt-log", resolveDbPath: resolveAttemptLogDbPath, open: initAttemptLog }, - { - name: "replay-snapshot", - // resolveReplaySnapshotDbPath's own (not-yet-converted) .d.ts types `env` as `NodeJS.ProcessEnv`, unlike - // every sibling resolver here (`Record`) -- a pre-existing inconsistency, not - // introduced by this batch. process.env genuinely satisfies both shapes at runtime, so this cast is safe. - resolveDbPath: resolveReplaySnapshotDbPath, - open: openReplaySnapshotStore, - }, - { - name: "worktree-allocator", - resolveDbPath: resolveWorktreeAllocatorDbPath, - open: (dbPath) => openWorktreeAllocator({ dbPath }), - }, - { - name: "contribution-profile", - resolveDbPath: resolveContributionProfileCacheDbPath, - open: initContributionProfileCache, - }, - { name: "policy-verdict-cache", resolveDbPath: resolvePolicyVerdictCacheDbPath, open: initPolicyVerdictCacheStore }, - { name: "policy-doc-cache", resolveDbPath: resolvePolicyDocCacheDbPath, open: initPolicyDocCacheStore }, -]; -/** Read a store file's stamped schema version without ever creating it -- matches checkStoreIntegrity's - * "not created yet" convention: an absent file has nothing to report a version for. */ -function peekSchemaVersion(dbPath) { - const db = new DatabaseSync(dbPath, { readOnly: true }); - try { - return readSchemaVersion(db); - } - finally { - db.close(); - } -} -/** - * Bring one store's EXISTING on-disk schema up to date. Never throws: a store that fails to open/migrate is - * reported as a failed result so one bad store cannot abort the whole sweep, matching doctor's per-store - * isolation. A store file that does not exist yet is reported as a clean skip (nothing to migrate), never - * created as a side effect of running this command. - */ -function migrateStore({ name, resolveDbPath, open }, env) { - const dbPath = resolveDbPath(env); - if (!existsSync(dbPath)) { - return { - name, - dbPath, - ok: true, - status: "skipped", - detail: "not created yet", - versionBefore: null, - versionAfter: null, - }; - } - // versionBefore is read INSIDE the same try as the migration itself: a corrupted file can throw on this very - // first read (a store that can't even be opened has no readable version either), and that must still surface - // as one failed store result rather than an uncaught exception aborting the whole sweep. - let versionBefore = null; - try { - versionBefore = peekSchemaVersion(dbPath); - const store = open(dbPath); - store.close(); - const versionAfter = peekSchemaVersion(dbPath); - return { - name, - dbPath, - ok: true, - status: versionAfter > versionBefore ? "migrated" : "up-to-date", - detail: `v${versionBefore} -> v${versionAfter}`, - versionBefore, - versionAfter, - }; - } - catch (error) { - // applySchemaMigrations applies AND stamps each migration in its OWN transaction, so a failure part-way - // through a multi-migration sequence leaves the file at the LAST fully-applied version -- genuinely AHEAD - // of versionBefore. Re-read the real on-disk version instead of reporting a misleading "nothing changed". - // Guarded by its own try: the failure may itself be an unreadable/corrupt file (the same reason - // versionBefore can still be null here), in which case the pre-failure reading is all we can honestly - // report. - let versionAfter = versionBefore; - try { - versionAfter = peekSchemaVersion(dbPath); - } - catch { - versionAfter = versionBefore; - } - return { - name, - dbPath, - ok: false, - status: "failed", - detail: error instanceof Error ? error.message : String(error), - versionBefore, - versionAfter, - }; - } -} -/** `stores` is injectable so tests can exercise a store descriptor's failure paths (e.g. a non-Error throw) - * without depending on real node:sqlite error shapes; defaults to the real seven-store list. */ -export function runMigrateChecks(env = process.env, stores = STORES) { - return stores.map((store) => migrateStore(store, env)); -} -export function runMigrate(args = [], env = process.env) { - const json = argsWantJson(args); - // Validated BEFORE any store is opened: a typo'd flag must fail fast rather than silently run a full - // migration sweep that ignored what the operator actually typed (#5917). `--json` is the only flag this - // command takes, so anything else -- an unrecognized flag or a stray positional -- is rejected. - const unknown = args.find((token) => token !== "--json"); - if (unknown !== undefined) - return reportCliFailure(json, `Unknown option: ${unknown}. ${MIGRATE_USAGE}`, 2); - const results = runMigrateChecks(env); - const failed = results.filter((result) => !result.ok); - if (json) { - console.log(JSON.stringify({ ok: failed.length === 0, stores: results }, null, 2)); - } - else { - for (const result of results) { - console.log(`${result.ok ? result.status.padEnd(10) : "FAIL "} ${result.name}: ${result.detail}`); - } - if (failed.length > 0) - console.error(`migrate: ${failed.length} store(s) failed`); - } - return failed.length === 0 ? 0 : 1; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlncmF0ZS1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtaWdyYXRlLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw2R0FBNkc7QUFDN0csNkdBQTZHO0FBQzdHLDhHQUE4RztBQUM5RywrR0FBK0c7QUFDL0cseUdBQXlHO0FBQ3pHLDJHQUEyRztBQUMzRyxnSEFBZ0g7QUFDaEgsMkdBQTJHO0FBQzNHLDhHQUE4RztBQUM5RyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3JDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDeEQsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxlQUFlLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM5RSxPQUFPLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDOUUsT0FBTyxFQUFFLGtCQUFrQixFQUFFLDJCQUEyQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdkYsT0FBTyxFQUFFLG9CQUFvQixFQUFFLDZCQUE2QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDN0YsT0FBTyxFQUFFLHVCQUF1QixFQUFFLDJCQUEyQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDNUYsT0FBTyxFQUFFLGlCQUFpQixFQUFFLHFCQUFxQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDMUUsT0FBTyxFQUFFLGFBQWEsRUFBRSxzQkFBc0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQ3BGLE9BQU8sRUFBRSxjQUFjLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/miner-goal-spec.d.ts b/packages/loopover-miner/lib/miner-goal-spec.d.ts deleted file mode 100644 index 864187641f..0000000000 --- a/packages/loopover-miner/lib/miner-goal-spec.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Stats } from "node:fs"; -import type { ParsedMinerGoalSpec } from "@loopover/engine"; -export type ResolveMinerGoalSpecOptions = { - existsSync?: (path: string) => boolean; - openSync?: (path: string, flags: number) => number; - fstatSync?: (fd: number) => Stats; - readSync?: (fd: number, buffer: Buffer, offset: number, length: number, position: number | null) => number; - closeSync?: (fd: number) => void; -}; -/** - * Resolve the real, parsed MinerGoalSpec for an already-cloned repo at `repoPath`, trying each - * MINER_GOAL_SPEC_FILENAMES candidate in the documented discovery order. Never throws: a missing file, an - * unreadable file, or malformed content all degrade to the tolerant parser's own absent/safe-default result. - * - * Injected filesystem operations receive the FULL joined path (same convention as `node:fs`'s own - * functions), not a repoPath-relative candidate. - */ -export declare function resolveMinerGoalSpec(repoPath: string, options?: ResolveMinerGoalSpecOptions): ParsedMinerGoalSpec; diff --git a/packages/loopover-miner/lib/miner-goal-spec.js b/packages/loopover-miner/lib/miner-goal-spec.js deleted file mode 100644 index 35d0cd9392..0000000000 --- a/packages/loopover-miner/lib/miner-goal-spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import { closeSync, constants, existsSync, fstatSync, openSync, readSync } from "node:fs"; -import { join } from "node:path"; -import { discoverMinerGoalSpecPath, parseMinerGoalSpecContent } from "@loopover/engine"; -const MAX_MINER_GOAL_SPEC_BYTES = 32_768; -// Same convention as packages/loopover-mcp/bin/loopover-mcp.js's readCliTextFile: O_NOFOLLOW on open -// atomically rejects a symlinked path (no separate pre-open lstat -- that would be a check-then-open race, since -// a symlink can be swapped in between the lstat and the open). Bounds the READ itself, not just fstat's -// reported size, since a regular file can still grow between fstatSync and the read below. -function readRegularUtf8File(path, options) { - const openImpl = options.openSync ?? openSync; - const fstatImpl = options.fstatSync ?? fstatSync; - const readImpl = options.readSync ?? readSync; - const closeImpl = options.closeSync ?? closeSync; - const fd = openImpl(path, constants.O_RDONLY | constants.O_NOFOLLOW); - try { - const stats = fstatImpl(fd); - if (!stats.isFile() || stats.size > MAX_MINER_GOAL_SPEC_BYTES) - return null; - const buffer = Buffer.alloc(MAX_MINER_GOAL_SPEC_BYTES + 1); - let bytesRead = 0; - while (bytesRead < buffer.length) { - const n = readImpl(fd, buffer, bytesRead, buffer.length - bytesRead, null); - if (n === 0) - break; - bytesRead += n; - } - if (bytesRead > MAX_MINER_GOAL_SPEC_BYTES) - return null; - return buffer.subarray(0, bytesRead).toString("utf8"); - } - finally { - closeImpl(fd); - } -} -/** - * Resolve the real, parsed MinerGoalSpec for an already-cloned repo at `repoPath`, trying each - * MINER_GOAL_SPEC_FILENAMES candidate in the documented discovery order. Never throws: a missing file, an - * unreadable file, or malformed content all degrade to the tolerant parser's own absent/safe-default result. - * - * Injected filesystem operations receive the FULL joined path (same convention as `node:fs`'s own - * functions), not a repoPath-relative candidate. - */ -export function resolveMinerGoalSpec(repoPath, options = {}) { - const existsImpl = options.existsSync ?? existsSync; - const relativePath = discoverMinerGoalSpecPath((candidate) => existsImpl(join(repoPath, candidate))); - if (!relativePath) - return parseMinerGoalSpecContent(null); - try { - const content = readRegularUtf8File(join(repoPath, relativePath), options); - return parseMinerGoalSpecContent(content); - } - catch { - return parseMinerGoalSpecContent(null); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWluZXItZ29hbC1zcGVjLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibWluZXItZ29hbC1zcGVjLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUUxRixPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBR3hGLE1BQU0seUJBQXlCLEdBQUcsTUFBTSxDQUFDO0FBa0J6QyxxR0FBcUc7QUFDckcsaUhBQWlIO0FBQ2pILHdHQUF3RztBQUN4RywyRkFBMkY7QUFDM0YsU0FBUyxtQkFBbUIsQ0FBQyxJQUFZLEVBQUUsT0FBb0M7SUFDN0UsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxRQUFRLENBQUM7SUFDOUMsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLFNBQVMsSUFBSSxTQUFTLENBQUM7SUFDakQsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxRQUFRLENBQUM7SUFDOUMsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLFNBQVMsSUFBSSxTQUFTLENBQUM7SUFFakQsTUFBTSxFQUFFLEdBQUcsUUFBUSxDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsUUFBUSxHQUFHLFNBQVMsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNyRSxJQUFJLENBQUM7UUFDSCxNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDNUIsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsSUFBSSxLQUFLLENBQUMsSUFBSSxHQUFHLHlCQUF5QjtZQUFFLE9BQU8sSUFBSSxDQUFDO1FBQzNFLE1BQU0sTUFBTSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMseUJBQXlCLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDM0QsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFDO1FBQ2xCLE9BQU8sU0FBUyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUNqQyxNQUFNLENBQUMsR0FBRyxRQUFRLENBQUMsRUFBRSxFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLE1BQU0sR0FBRyxTQUFTLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDM0UsSUFBSSxDQUFDLEtBQUssQ0FBQztnQkFBRSxNQUFNO1lBQ25CLFNBQVMsSUFBSSxDQUFDLENBQUM7UUFDakIsQ0FBQztRQUNELElBQUksU0FBUyxHQUFHLHlCQUF5QjtZQUFFLE9BQU8sSUFBSSxDQUFDO1FBQ3ZELE9BQU8sTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsU0FBUyxDQUFDLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3hELENBQUM7WUFBUyxDQUFDO1FBQ1QsU0FBUyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ2hCLENBQUM7QUFDSCxDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxvQkFBb0IsQ0FBQyxRQUFnQixFQUFFLFVBQXVDLEVBQUU7SUFDOUYsTUFBTSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsSUFBSSxVQUFVLENBQUM7SUFFcEQsTUFBTSxZQUFZLEdBQUcseUJBQXlCLENBQUMsQ0FBQyxTQUFTLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNyRyxJQUFJLENBQUMsWUFBWTtRQUFFLE9BQU8seUJBQXlCLENBQUMsSUFBSSxDQUFDLENBQUM7SUFFMUQsSUFBSSxDQUFDO1FBQ0gsTUFBTSxPQUFPLEdBQUcsbUJBQW1CLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxZQUFZLENBQUMsRUFBRSxPQUFPLENBQUMsQ0FBQztRQUMzRSxPQUFPLHlCQUF5QixDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzVDLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLHlCQUF5QixDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3pDLENBQUM7QUFDSCxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/oauth-device-flow.d.ts b/packages/loopover-miner/lib/oauth-device-flow.d.ts deleted file mode 100644 index bc0200285d..0000000000 --- a/packages/loopover-miner/lib/oauth-device-flow.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type DeviceCode = { - deviceCode: string; - userCode: string; - verificationUri: string; - expiresInSeconds: number; - intervalSeconds: number; -}; -export type DeviceFlowTokenResult = { - accessToken: string; - scope: string; -}; -/** The centrally-held loopover-ams App's OAuth client id -- public (not secret), so it's safe to read from a - * plain env var. Empty/unset means device-flow authorization isn't available in this build/deployment. */ -export declare function resolveAmsOauthClientId(env?: Record): string; -export declare class DeviceFlowError extends Error { - code: string; - constructor(code: string, message?: string); -} -/** Step 1 of the device flow: request a device code + the short user-facing code from GitHub. */ -export declare function requestDeviceCode({ clientId, scope, fetchFn, }?: { - clientId: string; - scope?: string; - fetchFn?: typeof fetch; -}): Promise; -/** - * Step 2: poll for the access token, honoring GitHub's device-flow polling protocol -- - * `authorization_pending` keeps polling at the current interval, `slow_down` increases it (to GitHub's own - * requested value when given), `expired_token`/`access_denied` are terminal failures, anything else is an - * unexpected terminal failure. Bounded by `expiresInSeconds` so a caller can never poll forever. - */ -export declare function pollForAccessToken({ clientId, deviceCode, intervalSeconds, expiresInSeconds, fetchFn, sleepFn, now, }?: { - clientId: string; - deviceCode: string; - intervalSeconds?: number; - expiresInSeconds?: number; - fetchFn?: typeof fetch; - sleepFn?: (ms: number) => Promise; - now?: () => number; -}): Promise; -/** - * Run the full device-flow authorization end to end: request a code, hand it to the caller's `onCode` (so the - * caller can display it however it likes -- CLI text, structured JSON, etc.), then poll until the user - * completes, declines, or the code expires. Returns the resulting access token; throws a DeviceFlowError on - * any failure -- the caller decides whether to fall back to another auth method. - */ -export declare function runDeviceFlowAuthorization({ clientId, scope, onCode, fetchFn, sleepFn, now, }: { - clientId: string; - scope?: string; - onCode: (code: DeviceCode) => void | Promise; - fetchFn?: typeof fetch; - sleepFn?: (ms: number) => Promise; - now?: () => number; -}): Promise; diff --git a/packages/loopover-miner/lib/oauth-device-flow.js b/packages/loopover-miner/lib/oauth-device-flow.js deleted file mode 100644 index 0eaf9c4655..0000000000 --- a/packages/loopover-miner/lib/oauth-device-flow.js +++ /dev/null @@ -1,133 +0,0 @@ -// GitHub OAuth Device Flow client (#5682) for the centrally-held `loopover-ams` GitHub App -- lets a -// contributor authorize loopover-miner by visiting a URL and entering a short code, instead of generating -// and pasting a PAT. Uses GitHub's PUBLIC-client device flow -// (https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow): no -// client secret is required or ever held by this CLI, only the App's public OAuth client id. -// -// The resulting user-to-server access token acts AS the authorizing human's own account, within their own -// GitHub permissions -- the exact same identity/attribution as a manually pasted PAT (see LOCAL_WRITE_BOUNDARY -// in @loopover/engine's local-write-tools.ts). This is deliberately NOT the installation-token mechanism Orb -// uses: an installation token requires the repo owner to install the App on their own repo, which is -// mechanically incompatible with contributing to third-party repos AMS doesn't own. -const DEVICE_CODE_URL = "https://github.com/login/device/code"; -const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; -const DEFAULT_SCOPE = "repo"; -const DEFAULT_EXPIRES_IN_SECONDS = 900; -const DEFAULT_INTERVAL_SECONDS = 5; -// #miner-github-read-timeouts: matches github-token-resolution.js's GITHUB_TOKEN_FETCH_TIMEOUT_MS -- a stalled -// connection can't hang forever, here or anywhere else this package talks to GitHub. -const DEVICE_FLOW_FETCH_TIMEOUT_MS = 10_000; -/** The centrally-held loopover-ams App's OAuth client id -- public (not secret), so it's safe to read from a - * plain env var. Empty/unset means device-flow authorization isn't available in this build/deployment. */ -export function resolveAmsOauthClientId(env = process.env) { - return typeof env.LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID === "string" ? env.LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID.trim() : ""; -} -export class DeviceFlowError extends Error { - code; - constructor(code, message) { - super(message || code); - this.code = code; - } -} -function defaultSleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -/** Step 1 of the device flow: request a device code + the short user-facing code from GitHub. */ -export async function requestDeviceCode({ clientId, scope = DEFAULT_SCOPE, fetchFn = fetch, } = {}) { - if (!clientId) - throw new DeviceFlowError("missing_client_id", "no OAuth client id configured for device-flow authorization"); - // Cast: ambient fetch is CF-Workers-flavored; this module only POSTs string URLs. - const resolvedFetch = fetchFn; - const res = await resolvedFetch(DEVICE_CODE_URL, { - method: "POST", - headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ client_id: clientId, scope }).toString(), - signal: AbortSignal.timeout(DEVICE_FLOW_FETCH_TIMEOUT_MS), - }); - if (!res.ok) - throw new DeviceFlowError("device_code_request_failed", `GitHub returned HTTP ${res.status} requesting a device code`); - const data = (await res.json()); - if (!data || typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string") { - throw new DeviceFlowError("device_code_response_invalid", "GitHub's device-code response was missing required fields"); - } - return { - deviceCode: data.device_code, - userCode: data.user_code, - verificationUri: data.verification_uri, - expiresInSeconds: typeof data.expires_in === "number" ? data.expires_in : DEFAULT_EXPIRES_IN_SECONDS, - intervalSeconds: typeof data.interval === "number" ? data.interval : DEFAULT_INTERVAL_SECONDS, - }; -} -/** - * Step 2: poll for the access token, honoring GitHub's device-flow polling protocol -- - * `authorization_pending` keeps polling at the current interval, `slow_down` increases it (to GitHub's own - * requested value when given), `expired_token`/`access_denied` are terminal failures, anything else is an - * unexpected terminal failure. Bounded by `expiresInSeconds` so a caller can never poll forever. - */ -export async function pollForAccessToken({ clientId, deviceCode, intervalSeconds = DEFAULT_INTERVAL_SECONDS, expiresInSeconds = DEFAULT_EXPIRES_IN_SECONDS, fetchFn = fetch, sleepFn = defaultSleep, now = () => Date.now(), } = {}) { - const deadline = now() + expiresInSeconds * 1000; - let interval = intervalSeconds; - // Cast: ambient fetch is CF-Workers-flavored; this module only POSTs string URLs. - const resolvedFetch = fetchFn; - for (;;) { - if (now() >= deadline) - throw new DeviceFlowError("expired_token", "the device code expired before authorization completed"); - await sleepFn(interval * 1000); - let res; - try { - res = await resolvedFetch(ACCESS_TOKEN_URL, { - method: "POST", - headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: clientId, - device_code: deviceCode, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }).toString(), - signal: AbortSignal.timeout(DEVICE_FLOW_FETCH_TIMEOUT_MS), - }); - } - catch { - // A stalled/timed-out attempt is a per-attempt failure, not a fatal one -- the existing deadline check - // at the top of the loop still bounds total polling time, so this just costs one wasted interval. - continue; - } - const data = (await res.json().catch(() => ({}))); - if (data && typeof data.access_token === "string" && data.access_token) { - return { accessToken: data.access_token, scope: typeof data.scope === "string" ? data.scope : "" }; - } - const error = data && typeof data.error === "string" ? data.error : null; - if (error === "authorization_pending") - continue; - if (error === "slow_down") { - interval = typeof data.interval === "number" ? data.interval : interval + 5; - continue; - } - if (error === "expired_token") - throw new DeviceFlowError("expired_token", "the device code expired before authorization completed"); - if (error === "access_denied") - throw new DeviceFlowError("access_denied", "authorization was declined"); - throw new DeviceFlowError(error || "device_flow_failed", (typeof data.error_description === "string" ? data.error_description : undefined) || - `unexpected device-flow response (HTTP ${res.status})`); - } -} -/** - * Run the full device-flow authorization end to end: request a code, hand it to the caller's `onCode` (so the - * caller can display it however it likes -- CLI text, structured JSON, etc.), then poll until the user - * completes, declines, or the code expires. Returns the resulting access token; throws a DeviceFlowError on - * any failure -- the caller decides whether to fall back to another auth method. - */ -export async function runDeviceFlowAuthorization({ clientId, scope, onCode, fetchFn = fetch, sleepFn, now, }) { - // Cast: optional scope/sleepFn/now may be undefined; keep the JS's always-pass shape under EOPT. - const code = await requestDeviceCode({ clientId, scope, fetchFn }); - await onCode(code); - return pollForAccessToken({ - clientId, - deviceCode: code.deviceCode, - intervalSeconds: code.intervalSeconds, - expiresInSeconds: code.expiresInSeconds, - fetchFn, - sleepFn, - now, - }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2F1dGgtZGV2aWNlLWZsb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJvYXV0aC1kZXZpY2UtZmxvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxxR0FBcUc7QUFDckcsMEdBQTBHO0FBQzFHLDZEQUE2RDtBQUM3RCwwR0FBMEc7QUFDMUcsNkZBQTZGO0FBQzdGLEVBQUU7QUFDRiwwR0FBMEc7QUFDMUcsK0dBQStHO0FBQy9HLDZHQUE2RztBQUM3RyxxR0FBcUc7QUFDckcsb0ZBQW9GO0FBRXBGLE1BQU0sZUFBZSxHQUFHLHNDQUFzQyxDQUFDO0FBQy9ELE1BQU0sZ0JBQWdCLEdBQUcsNkNBQTZDLENBQUM7QUFDdkUsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDO0FBQzdCLE1BQU0sMEJBQTBCLEdBQUcsR0FBRyxDQUFDO0FBQ3ZDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxDQUFDO0FBQ25DLCtHQUErRztBQUMvRyxxRkFBcUY7QUFDckYsTUFBTSw0QkFBNEIsR0FBRyxNQUFNLENBQUM7QUFlNUM7MkdBQzJHO0FBQzNHLE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUMzRixPQUFPLE9BQU8sR0FBRyxDQUFDLGtDQUFrQyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLGtDQUFrQyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7QUFDekgsQ0FBQztBQUVELE1BQU0sT0FBTyxlQUFnQixTQUFRLEtBQUs7SUFDeEMsSUFBSSxDQUFTO0lBQ2IsWUFBWSxJQUFZLEVBQUUsT0FBZ0I7UUFDeEMsS0FBSyxDQUFDLE9BQU8sSUFBSSxJQUFJLENBQUMsQ0FBQztRQUN2QixJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztJQUNuQixDQUFDO0NBQ0Y7QUFFRCxTQUFTLFlBQVksQ0FBQyxFQUFVO0lBQzlCLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/opportunity-fanout.d.ts b/packages/loopover-miner/lib/opportunity-fanout.d.ts deleted file mode 100644 index 4fd88e0f6a..0000000000 --- a/packages/loopover-miner/lib/opportunity-fanout.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ForgeConfig } from "./forge-config.js"; -import type { PolicyDocCache } from "./policy-doc-cache.js"; -import type { PolicyVerdictCache } from "./policy-verdict-cache.js"; -export type FanoutTarget = { - owner: string; - repo: string; -}; -/** Options shared by every fan-out entry point. `apiBaseUrl` is the legacy top-level forge-host override (it still - * wins over `forge.apiBaseUrl`); `forge` (#4784) carries the rest of the per-tenant forge knobs. `policyDocCache`, - * when supplied, lets discovery revalidate each repo's policy docs with a conditional GET instead of a full - * refetch (#4842). `policyVerdictCache`, when supplied, lets discovery reuse an already-resolved verdict once its - * deciding doc's ETag is confirmed unchanged, instead of re-resolving it (#4843). */ -export type FanoutOptions = { - apiBaseUrl?: string; - forge?: Partial; - concurrency?: number; - rateLimitLowWaterMark?: number; - rateLimitHighWaterMark?: number; - perPage?: number; - maxPages?: number; - requestTimeoutMs?: number; - sleepFn?: (ms: number) => Promise; - policyDocCache?: PolicyDocCache | null; - policyVerdictCache?: PolicyVerdictCache | null; -}; -export type RawCandidateIssue = { - owner: string; - repo: string; - repoFullName: string; - issueNumber: number; - title: string; - labels: string[]; - /** Assignee logins (#7040), already present in the same list/search payload as labels — no extra request. */ - assignees: string[]; - commentsCount: number; - createdAt: string | null; - updatedAt: string | null; - htmlUrl: string | null; - aiPolicyAllowed: true; - aiPolicySource: "AI-USAGE.md" | "CONTRIBUTING.md" | "none"; -}; -export type CandidateIssueWarning = { - repoFullName: string; - stage: string; - message: string; -}; -export type CandidateIssueSummary = { - issues: RawCandidateIssue[]; - rateLimitRemaining: number | null; - rateLimitResetAt: string | null; - warnings: CandidateIssueWarning[]; -}; -export declare function mapWithConcurrency(items: T[], maxConcurrency: number, worker: (item: T, index: number) => Promise, resolveLimit: () => number, sleepFn?: (ms: number) => Promise): Promise; -export declare function fetchCandidateIssuesWithSummary(targets: FanoutTarget[], githubToken: string, options?: FanoutOptions): Promise; -/** - * Metadata-only GitHub discovery (#2307): never clones source, never fetches blobs beyond small policy docs, - * never uploads source, and never performs writes. Call the WithSummary variant when rate-limit telemetry is - * needed. - */ -export declare function fetchCandidateIssues(targets: FanoutTarget[], githubToken: string, options?: FanoutOptions): Promise; -export declare function searchCandidateIssuesWithSummary(searchQuery: string, githubToken: string, options?: FanoutOptions): Promise; -export declare function searchCandidateIssues(searchQuery: string, githubToken: string, options?: FanoutOptions): Promise; diff --git a/packages/loopover-miner/lib/opportunity-fanout.js b/packages/loopover-miner/lib/opportunity-fanout.js deleted file mode 100644 index 10ab566243..0000000000 --- a/packages/loopover-miner/lib/opportunity-fanout.js +++ /dev/null @@ -1,544 +0,0 @@ -import { Buffer } from "node:buffer"; -import { resolveAiPolicyVerdict } from "@loopover/engine"; -import { resolveForgeConfig } from "./forge-config.js"; -import { DEFAULT_RATE_LIMIT_HIGH_WATER_MARK, DEFAULT_RATE_LIMIT_LOW_WATER_MARK, resolveThrottledConcurrency, } from "./discovery-throttle.js"; -import { fetchWithRetry } from "./http-retry.js"; -const defaultConcurrency = 5; -// How long a parked worker waits before re-checking the live rate-limit-derived concurrency limit (#4844). -const throttleParkMs = 25; -const defaultPerPage = 100; -// Follow the GitHub Link header past the first page so a repo/search with >100 open issues isn't silently -// truncated (#4831); cap the follow loop so a pathological Link chain can't run away. -const defaultMaxPages = 10; -const defaultRequestTimeoutMs = 10_000; -function normalizeLimit(value, fallback, min, max) { - if (!Number.isFinite(value)) - return fallback; - return Math.min(max, Math.max(min, Math.floor(value))); -} -function targetKey(target) { - return `${target.owner.toLowerCase()}/${target.repo.toLowerCase()}`; -} -function normalizeTargets(targets) { - const seen = new Set(); - const normalized = []; - for (const target of Array.isArray(targets) ? targets : []) { - const owner = typeof target?.owner === "string" ? target.owner.trim() : ""; - const repo = typeof target?.repo === "string" ? target.repo.trim() : ""; - if (!owner || !repo) - continue; - const key = targetKey({ owner, repo }); - if (seen.has(key)) - continue; - seen.add(key); - normalized.push({ owner, repo, repoFullName: `${owner}/${repo}` }); - } - return normalized; -} -function targetFromFullName(fullName) { - if (typeof fullName !== "string") - return null; - const [owner, repo, extra] = fullName.split("/"); - if (!owner || !repo || extra) - return null; - return { owner, repo, repoFullName: `${owner}/${repo}` }; -} -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} -// Derive owner/repo from a search hit when `repository.full_name` is absent, using the tenant forge's own -// `repoPathPrefix` for the API `repository_url` and a forge-agnostic host for the web `html_url` (#4784). Hardcoding -// `/repos/` and `github.com` here dropped every custom-forge search result whose payload omitted `full_name`. -function targetFromSearchIssue(issue, forge) { - const repositoryFullName = targetFromFullName(issue?.repository?.full_name); - if (repositoryFullName) - return repositoryFullName; - const repoPathPrefix = escapeRegExp(forge.repoPathPrefix.replace(/\/+$/, "")); - const repositoryUrl = typeof issue?.repository_url === "string" - ? issue.repository_url.match(new RegExp(`${repoPathPrefix}/([^/?#]+)/([^/?#]+)(?:[?#].*)?$`)) - : null; - if (repositoryUrl) { - const owner = decodeURIComponent(repositoryUrl[1]); - const repo = decodeURIComponent(repositoryUrl[2]); - return { owner, repo, repoFullName: `${owner}/${repo}` }; - } - const htmlUrl = typeof issue?.html_url === "string" - ? issue.html_url.match(/^https:\/\/[^/]+\/([^/]+)\/([^/]+)\/issues\/\d+(?:[?#].*)?$/) - : null; - if (htmlUrl) { - const owner = decodeURIComponent(htmlUrl[1]); - const repo = decodeURIComponent(htmlUrl[2]); - return { owner, repo, repoFullName: `${owner}/${repo}` }; - } - return null; -} -function githubHeaders(githubToken, forge) { - const headers = { - accept: forge.acceptHeader, - "user-agent": forge.userAgent, - [forge.apiVersionHeader]: forge.apiVersion, - }; - const token = typeof githubToken === "string" ? githubToken.trim() : ""; - if (token) - headers.authorization = `Bearer ${token}`; - return headers; -} -function apiUrl(apiBaseUrl, path, query = "") { - return `${apiBaseUrl.replace(/\/+$/, "")}${path}${query}`; -} -function repoPath(forge, target, suffix) { - return `${forge.repoPathPrefix}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}${suffix}`; -} -function recordRateLimit(summary, response) { - const remaining = Number(response.headers.get("x-ratelimit-remaining")); - if (Number.isFinite(remaining)) { - summary.rateLimitRemaining = - summary.rateLimitRemaining === null - ? remaining - : Math.min(summary.rateLimitRemaining, remaining); - } - const resetSeconds = Number(response.headers.get("x-ratelimit-reset")); - if (Number.isFinite(resetSeconds) && resetSeconds > 0) { - const resetAt = new Date(resetSeconds * 1000).toISOString(); - summary.rateLimitResetAt = - summary.rateLimitResetAt === null || resetAt > summary.rateLimitResetAt - ? resetAt - : summary.rateLimitResetAt; - } -} -async function githubGetJson(url, githubToken, summary, options, extraHeaders = {}) { - // Retry a transient 5xx from GitHub before dropping this target's results for the whole run (#4830) — the same - // discipline as the CI/gate-verdict pollers. A thrown network error still propagates to each caller's try/catch. - // `extraHeaders` carries per-call additions (e.g. a policy-doc If-None-Match, #4842) on top of the base auth set. - // requestTimeoutMs bounds each individual attempt so a stalled connection can't hang discovery forever - // (#miner-github-read-timeouts) -- fetchWithRetry gives each retry its own fresh AbortSignal.timeout(). - // Spread-omit sleepFn when absent rather than passing `sleepFn: undefined` -- FetchWithRetryOptions doesn't - // widen its optional properties to `| undefined`, and exactOptionalPropertyTypes treats those as different. - const retryOptions = { ...(options?.sleepFn ? { sleepFn: options.sleepFn } : {}), timeoutMs: options?.requestTimeoutMs }; - const response = await fetchWithRetry(fetch, url, { method: "GET", headers: { ...githubHeaders(githubToken, options.forge), ...extraHeaders } }, retryOptions); - recordRateLimit(summary, response); - const payload = await response.json().catch(() => null); - return { response, payload }; -} -function decodeContentPayload(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return null; - const content = payload; - if (typeof content.content !== "string") - return null; - if (content.encoding === "base64") { - return Buffer.from(content.content.replace(/\s/g, ""), "base64").toString("utf8"); - } - return content.content; -} -function warning(target, stage, message) { - return { repoFullName: target.repoFullName, stage, message }; -} -// Read a URL's prior ETag so an unchanged doc can be revalidated with a conditional GET (#4842). A cache that is -// absent, or whose read throws (corrupt/locked file), is treated as a plain miss: the caller does a full fetch, -// per the "never risk a stale policy" rule — the cache only ever makes discovery cheaper, never less correct. -function readCachedPolicyDoc(cache, url) { - if (!cache) - return null; - try { - return cache.get(url); - } - catch { - return null; - } -} -function readEtagHeader(response) { - const etag = response.headers.get("etag"); - return typeof etag === "string" && etag.trim() ? etag : null; -} -// Persist the fresh ETag + body so the NEXT discover run can revalidate instead of re-downloading. Only a real -// ETag paired with decoded content is stored, and a write that throws must never fail discovery (same stale-safe -// rule) — it degrades to "not cached", so the next run simply refetches in full. -function writeCachedPolicyDoc(cache, url, etag, content) { - if (!cache || content === null || etag === null) - return; - try { - cache.put(url, etag, content); - } - catch { - // Leave this URL uncached; the next run refetches fully rather than serving anything stale. - } -} -// A bare `owner/repo` is NOT a safe policy-verdict cache key: two different tenant forge hosts (#4784's -// per-tenant `apiBaseUrl`) can each have their own unrelated repo of the same name, and their policy docs are -// wholly independent. Scope the key by host, mirroring policy-doc-cache.js's own precedent of keying on the full -// request URL rather than a bare path. -function policyVerdictCacheKey(apiBaseUrl, repoFullName) { - return `${apiBaseUrl}::${repoFullName}`; -} -// Read a repo scope's previously-resolved verdict for the SAME decisive doc + ETag (#4843). A cache that is -// absent, or whose read throws (corrupt/locked file), is treated as a plain miss — the caller resolves the -// verdict fresh, per the same "never risk a stale policy" rule as the doc cache above. -function readCachedPolicyVerdict(cache, repoScope) { - if (!cache) - return null; - try { - return cache.get(repoScope); - } - catch { - return null; - } -} -// Persist the freshly-resolved verdict against the ETag of the doc that decided it, so the next run can reuse it -// outright once that ETag is confirmed unchanged. Only ever called with a real ETag; a write that throws must -// never fail discovery — it degrades to "not cached", so the next run just resolves the verdict again. -function writeCachedPolicyVerdict(cache, repoScope, decisiveDoc, etag, verdict) { - if (!cache || etag === null) - return; - try { - cache.put(repoScope, decisiveDoc, etag, verdict); - } - catch { - // Leave this repo scope uncached; the next run just resolves the verdict fresh again. - } -} -async function fetchRepoDoc(target, path, githubToken, options, summary, warnings) { - const url = apiUrl(options.apiBaseUrl, repoPath(options.forge, target, `/contents/${encodeURIComponent(path)}`)); - const cached = readCachedPolicyDoc(options.policyDocCache, url); - const conditionalHeaders = cached ? { "if-none-match": cached.etag } : {}; - try { - const { response, payload } = await githubGetJson(url, githubToken, summary, options, conditionalHeaders); - // A 304 only ever follows the If-None-Match we send above, which we only send when `cached` exists — so the - // cached body is the GitHub-confirmed current content, served with no extra rate-limit spend. - if (response.status === 304) - return { content: cached.content, etag: cached.etag }; - if (response.status === 404) - return { content: null, etag: null }; - if (!response.ok) { - warnings.push(warning(target, `policy:${path}`, `GitHub returned ${response.status}`)); - return { content: null, etag: null }; - } - const content = decodeContentPayload(payload); - const etag = readEtagHeader(response); - writeCachedPolicyDoc(options.policyDocCache, url, etag, content); - return { content, etag }; - } - catch (error) { - warnings.push(warning(target, `policy:${path}`, error instanceof Error ? error.message : "policy fetch failed")); - return { content: null, etag: null }; - } -} -// Resolve a repo scope's AI-usage-policy verdict, reusing a cached one when the deciding doc's ETag hasn't moved -// since it was last resolved (#4843). Only ever consulted with an ETag that a same-run conditional-GET just -// confirmed is current, so a cache hit is exactly as correct as recomputing — it just skips the (cheap, but not -// free) parse. -function resolveOrCacheVerdict(cache, repoScope, decisiveDoc, etag, computeVerdict) { - if (etag !== null) { - const cached = readCachedPolicyVerdict(cache, repoScope); - if (cached && cached.decisiveDoc === decisiveDoc && cached.etag === etag) - return cached.verdict; - } - const verdict = computeVerdict(); - writeCachedPolicyVerdict(cache, repoScope, decisiveDoc, etag, verdict); - return verdict; -} -async function resolveRepoAiPolicy(target, githubToken, options, summary, warnings) { - const repoScope = policyVerdictCacheKey(options.apiBaseUrl, target.repoFullName); - const { content: aiUsage, etag: aiUsageEtag } = await fetchRepoDoc(target, "AI-USAGE.md", githubToken, options, summary, warnings); - // Short-circuit only on AI-USAGE.md that has real content. A present-but-blank AI-USAGE.md must still fall - // through to CONTRIBUTING.md — otherwise a stub AI-USAGE.md silently fails open and swallows a ban declared in - // CONTRIBUTING.md (the exact case resolveAiPolicyVerdict was fixed to handle in #2900, which can only fire if - // both docs reach it). - if (aiUsage !== null && aiUsage.trim().length > 0) { - return resolveOrCacheVerdict(options.policyVerdictCache, repoScope, "AI-USAGE.md", aiUsageEtag, () => resolveAiPolicyVerdict({ aiUsage, contributing: null })); - } - const { content: contributing, etag: contributingEtag } = await fetchRepoDoc(target, "CONTRIBUTING.md", githubToken, options, summary, warnings); - return resolveOrCacheVerdict(options.policyVerdictCache, repoScope, "CONTRIBUTING.md", contributingEtag, () => resolveAiPolicyVerdict({ aiUsage: null, contributing })); -} -function labelNames(labels) { - if (!Array.isArray(labels)) - return []; - return labels - .map((label) => { - if (typeof label === "string") - return label; - if (label && typeof label === "object" && typeof label.name === "string") { - return label.name; - } - return ""; - }) - .filter((name) => name.length > 0); -} -// Assignee logins (#7040): GitHub's issue-list/search payloads already carry `assignees` in the same response -// that supplies labels/comments/etc. -- no extra request needed. contribution-profile-filter.js's -// assignee-exclusion rule uses this to drop candidates assigned to a login the target repo considers off-limits -// (its own owner, by default). -function assigneeLogins(assignees) { - if (!Array.isArray(assignees)) - return []; - return assignees - .map((assignee) => assignee && typeof assignee === "object" && typeof assignee.login === "string" - ? assignee.login - : "") - .filter((login) => login.length > 0); -} -function normalizeIssue(target, issue, policySource) { - if (!issue || typeof issue !== "object" || issue.pull_request) - return null; - const candidate = issue; - if (!Number.isInteger(candidate.number) || candidate.number <= 0) - return null; - if (typeof candidate.title !== "string" || candidate.title.trim().length === 0) - return null; - return { - owner: target.owner, - repo: target.repo, - repoFullName: target.repoFullName, - issueNumber: candidate.number, - title: candidate.title, - labels: labelNames(candidate.labels), - assignees: assigneeLogins(candidate.assignees), - commentsCount: Number.isFinite(candidate.comments) ? candidate.comments : 0, - createdAt: typeof candidate.created_at === "string" ? candidate.created_at : null, - updatedAt: typeof candidate.updated_at === "string" ? candidate.updated_at : null, - htmlUrl: typeof candidate.html_url === "string" ? candidate.html_url : null, - aiPolicyAllowed: true, - aiPolicySource: policySource, - }; -} -function searchQueryWithIssueQualifiers(searchQuery, forge) { - const trimmed = typeof searchQuery === "string" ? searchQuery.trim() : ""; - if (!trimmed) - return ""; - return `${trimmed} ${forge.searchQualifiers}`; -} -// The URL of the next page from a GitHub Link header (`; rel="next"`), constrained to the current -// token-bearing GitHub API endpoint so a forged Link header cannot redirect credentials off-origin. -function nextPageUrl(response, apiBaseUrl, expectedPath) { - const linkHeader = response.headers.get("link") ?? ""; - const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/); - if (match === null) - return null; - let nextUrl; - let expectedUrl; - try { - expectedUrl = new URL(apiUrl(apiBaseUrl, expectedPath)); - nextUrl = new URL(match[1], expectedUrl); - } - catch { - return null; - } - if (nextUrl.protocol !== "https:" || - nextUrl.origin !== expectedUrl.origin || - nextUrl.pathname !== expectedUrl.pathname) { - return null; - } - return nextUrl.toString(); -} -async function fetchTargetIssues(target, githubToken, options, summary, warnings) { - const verdict = await resolveRepoAiPolicy(target, githubToken, options, summary, warnings); - if (!verdict.allowed) - return []; - const issuesPath = repoPath(options.forge, target, "/issues"); - let url = apiUrl(options.apiBaseUrl, issuesPath, `?state=open&per_page=${options.perPage}`); - const issues = []; - try { - for (let page = 0; url !== null && page < options.maxPages; page += 1) { - const { response, payload } = await githubGetJson(url, githubToken, summary, options); - if (!response.ok) { - warnings.push(warning(target, "issues", `GitHub returned ${response.status}`)); - return issues; - } - if (!Array.isArray(payload)) { - warnings.push(warning(target, "issues", "GitHub returned a non-array issues payload")); - return issues; - } - for (const issue of payload) { - const normalized = normalizeIssue(target, issue, verdict.source); - if (normalized !== null) - issues.push(normalized); - } - url = nextPageUrl(response, options.apiBaseUrl, issuesPath); - } - return issues; - } - catch (error) { - warnings.push(warning(target, "issues", error instanceof Error ? error.message : "issue fetch failed")); - return issues; - } -} -async function fetchSearchIssues(searchQuery, githubToken, options, summary, warnings) { - const qualifiedQuery = searchQueryWithIssueQualifiers(searchQuery, options.forge); - if (!qualifiedQuery) - return []; - const searchPath = options.forge.searchEndpoint; - let url = apiUrl(options.apiBaseUrl, searchPath, `?q=${encodeURIComponent(qualifiedQuery)}&per_page=${options.perPage}`); - const items = []; - try { - for (let page = 0; url !== null && page < options.maxPages; page += 1) { - const { response, payload } = await githubGetJson(url, githubToken, summary, options); - if (!response.ok) { - warnings.push({ - repoFullName: "*", - stage: "search", - message: `GitHub returned ${response.status}`, - }); - return items; - } - const searchPayload = payload; - if (!searchPayload || typeof searchPayload !== "object" || !Array.isArray(searchPayload.items)) { - warnings.push({ - repoFullName: "*", - stage: "search", - message: "GitHub returned a non-array search payload", - }); - return items; - } - items.push(...searchPayload.items); - url = nextPageUrl(response, options.apiBaseUrl, searchPath); - } - return items; - } - catch (error) { - warnings.push({ - repoFullName: "*", - stage: "search", - message: error instanceof Error ? error.message : "issue search failed", - }); - return items; - } -} -function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -// Run `worker` over `items` with a dynamic in-flight cap (#4844). The pool spawns `maxConcurrency` loops, but a -// loop parks (re-checking every `throttleParkMs`) whenever the live `resolveLimit()` — derived from the recorded -// rate-limit budget — is already met by the number of in-flight workers, so effective concurrency tapers off as -// the budget drops instead of sprinting into a 403. `sleepFn` lets tests inject an instant wait for the park. -export async function mapWithConcurrency(items, maxConcurrency, worker, resolveLimit, sleepFn) { - const results = new Array(items.length); - const sleep = sleepFn ?? delay; - let next = 0; - let active = 0; - const runOne = async () => { - while (next < items.length) { - // Park while the live limit is already saturated. The check and the `active`/`next` bumps below run without - // an intervening await, so two loops can never claim the same slot. - while (active >= resolveLimit()) { - await sleep(throttleParkMs); - } - // The shared cursor can be drained by other loops while this one is parked, so re-check before claiming. - if (next >= items.length) - return; - const index = next; - next += 1; - active += 1; - try { - results[index] = await worker(items[index], index); - } - finally { - active -= 1; - } - } - }; - const workers = Array.from({ length: Math.min(maxConcurrency, items.length) }, runOne); - await Promise.all(workers); - return results; -} -/** A live limit resolver for `mapWithConcurrency`, reading the summary's rate-limit budget as it is updated (#4844). */ -function liveConcurrencyResolver(normalizedOptions, summary) { - return () => resolveThrottledConcurrency(normalizedOptions.concurrency, summary.rateLimitRemaining, normalizedOptions.rateLimitLowWaterMark, normalizedOptions.rateLimitHighWaterMark); -} -function normalizeOptions(options = {}) { - // A legacy top-level `apiBaseUrl` (the pre-#4784 GitHub-Enterprise override every existing caller uses) still wins - // over `forge.apiBaseUrl`, so nothing that already passes `apiBaseUrl` changes behavior. - const apiBaseUrlOverride = typeof options.apiBaseUrl === "string" && options.apiBaseUrl.trim() - ? { apiBaseUrl: options.apiBaseUrl } - : {}; - const forge = resolveForgeConfig({ ...(options.forge ?? {}), ...apiBaseUrlOverride }); - return { - forge, - apiBaseUrl: forge.apiBaseUrl, - concurrency: normalizeLimit(options.concurrency, defaultConcurrency, 1, 10), - // Below/above these recorded-rate-limit-remaining marks the fanout serializes / runs at full concurrency; in - // between it scales down linearly (#4844). - rateLimitLowWaterMark: normalizeLimit(options.rateLimitLowWaterMark, DEFAULT_RATE_LIMIT_LOW_WATER_MARK, 0, 1_000_000), - rateLimitHighWaterMark: normalizeLimit(options.rateLimitHighWaterMark, DEFAULT_RATE_LIMIT_HIGH_WATER_MARK, 1, 1_000_000), - perPage: normalizeLimit(options.perPage, defaultPerPage, 1, 100), - maxPages: normalizeLimit(options.maxPages, defaultMaxPages, 1, 100), - requestTimeoutMs: normalizeLimit(options.requestTimeoutMs, defaultRequestTimeoutMs, 1, 60_000), - // Passed through to the per-fetch retry so tests can inject an instant sleep; undefined uses the real backoff. - sleepFn: typeof options.sleepFn === "function" ? options.sleepFn : undefined, - // Optional local ETag cache for policy-doc revalidation (#4842). Absent (null) => every policy doc is fetched - // in full, exactly as before; discover-cli.js supplies the real on-disk store for a live run. - policyDocCache: options.policyDocCache ?? null, - // Optional local cache of resolved policy verdicts (#4843). Absent (null) => every verdict is resolved fresh, - // exactly as before; discover-cli.js supplies the real on-disk store for a live run. - policyVerdictCache: options.policyVerdictCache ?? null, - }; -} -export async function fetchCandidateIssuesWithSummary(targets, githubToken, options = {}) { - const normalizedOptions = normalizeOptions(options); - const normalizedTargets = normalizeTargets(targets); - const summary = { - rateLimitRemaining: null, - rateLimitResetAt: null, - }; - const warnings = []; - const batches = await mapWithConcurrency(normalizedTargets, normalizedOptions.concurrency, (target) => fetchTargetIssues(target, githubToken, normalizedOptions, summary, warnings), liveConcurrencyResolver(normalizedOptions, summary), normalizedOptions.sleepFn); - return { - issues: batches.flat(), - rateLimitRemaining: summary.rateLimitRemaining, - rateLimitResetAt: summary.rateLimitResetAt, - warnings, - }; -} -/** - * Metadata-only GitHub discovery (#2307): never clones source, never fetches blobs beyond small policy docs, - * never uploads source, and never performs writes. Call the WithSummary variant when rate-limit telemetry is - * needed. - */ -export async function fetchCandidateIssues(targets, githubToken, options = {}) { - const result = await fetchCandidateIssuesWithSummary(targets, githubToken, options); - return result.issues; -} -export async function searchCandidateIssuesWithSummary(searchQuery, githubToken, options = {}) { - const normalizedOptions = normalizeOptions(options); - const summary = { - rateLimitRemaining: null, - rateLimitResetAt: null, - }; - const warnings = []; - const searchItems = await fetchSearchIssues(searchQuery, githubToken, normalizedOptions, summary, warnings); - const targetsByKey = new Map(); - for (const item of searchItems) { - if (!item || typeof item !== "object" || item.pull_request) - continue; - const target = targetFromSearchIssue(item, normalizedOptions.forge); - if (target && !targetsByKey.has(targetKey(target))) - targetsByKey.set(targetKey(target), target); - } - const policyEntries = await mapWithConcurrency([...targetsByKey.values()], normalizedOptions.concurrency, async (target) => { - const verdict = await resolveRepoAiPolicy(target, githubToken, normalizedOptions, summary, warnings); - return [targetKey(target), verdict]; - }, liveConcurrencyResolver(normalizedOptions, summary), normalizedOptions.sleepFn); - const policiesByKey = new Map(policyEntries); - const issues = []; - for (const item of searchItems) { - const target = targetFromSearchIssue(item, normalizedOptions.forge); - if (!target) - continue; - const policy = policiesByKey.get(targetKey(target)); - if (!policy?.allowed) - continue; - const normalizedIssue = normalizeIssue(target, item, policy.source); - if (normalizedIssue) - issues.push(normalizedIssue); - } - return { - issues, - rateLimitRemaining: summary.rateLimitRemaining, - rateLimitResetAt: summary.rateLimitResetAt, - warnings, - }; -} -export async function searchCandidateIssues(searchQuery, githubToken, options = {}) { - const result = await searchCandidateIssuesWithSummary(searchQuery, githubToken, options); - return result.issues; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3Bwb3J0dW5pdHktZmFub3V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsib3Bwb3J0dW5pdHktZmFub3V0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFckMsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFMUQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFHdkQsT0FBTyxFQUNMLGtDQUFrQyxFQUNsQyxpQ0FBaUMsRUFDakMsMkJBQTJCLEdBQzVCLE1BQU0seUJBQXlCLENBQUM7QUFDakMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBa0ZqRCxNQUFNLGtCQUFrQixHQUFHLENBQUMsQ0FBQztBQUM3QiwyR0FBMkc7QUFDM0csTUFBTSxjQUFjLEdBQUcsRUFBRSxDQUFDO0FBQzFCLE1BQU0sY0FBYyxHQUFHLEdBQUcsQ0FBQztBQUMzQiwwR0FBMEc7QUFDMUcsc0ZBQXNGO0FBQ3RGLE1BQU0sZUFBZSxHQUFHLEVBQUUsQ0FBQztBQUMzQixNQUFNLHVCQUF1QixHQUFHLE1BQU0sQ0FBQztBQXVCdkMsU0FBUyxjQUFjLENBQUMsS0FBYyxFQUFFLFFBQWdCLEVBQUUsR0FBVyxFQUFFLEdBQVc7SUFDaEYsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDO1FBQUUsT0FBTyxRQUFRLENBQUM7SUFDN0MsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQWUsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNuRSxDQUFDO0FBRUQsU0FBUyxTQUFTLENBQUMsTUFBdUM7SUFDeEQsT0FBTyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxFQUFFLElBQUksTUFBTSxDQUFD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/opportunity-ranker.d.ts b/packages/loopover-miner/lib/opportunity-ranker.d.ts deleted file mode 100644 index 2c26eeecd8..0000000000 --- a/packages/loopover-miner/lib/opportunity-ranker.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { MinerGoalSpec } from "@loopover/engine"; -import type { RawCandidateIssue } from "./opportunity-fanout.js"; -export type RankedCandidateIssue = RawCandidateIssue & { - potential: number; - feasibility: number; - laneFit: number; - freshness: number; - dupRisk: number; - rankScore: number; -}; -export type RankCandidateIssuesOptions = { - nowMs?: number; - highRiskDuplicateClusters?: number; - openPullRequests?: number; - goalSpecsByRepo?: Record; - goalSpecContentByRepo?: Record; -}; -export type RankedCandidateSummary = { - issues: RankedCandidateIssue[]; - skippedInvalid: number; - usedDefaultGoalSpec: boolean; - defaultGoalSpec: MinerGoalSpec; -}; -/** - * Rank metadata-only fan-out candidates locally. Never clones source, never uploads metadata, and never writes to - * GitHub — it only composes deterministic engine signals and returns the sorted list. - */ -export declare function rankCandidateIssues(candidates: RawCandidateIssue[], options?: RankCandidateIssuesOptions): RankedCandidateIssue[]; -export declare function rankCandidateIssuesWithSummary(candidates: RawCandidateIssue[], options?: RankCandidateIssuesOptions): RankedCandidateSummary; diff --git a/packages/loopover-miner/lib/opportunity-ranker.js b/packages/loopover-miner/lib/opportunity-ranker.js deleted file mode 100644 index 136ef34d64..0000000000 --- a/packages/loopover-miner/lib/opportunity-ranker.js +++ /dev/null @@ -1,115 +0,0 @@ -import { DEFAULT_MINER_GOAL_SPEC, parseMinerGoalSpecContent, rankMetadataOpportunities, } from "@loopover/engine"; -function finiteEpochMs(value) { - return Number.isFinite(value) ? value : Date.now(); -} -function finiteNonNegativeInt(value) { - if (!Number.isFinite(value)) - return 0; - return Math.max(0, Math.floor(value)); -} -function normalizeCandidate(candidate) { - if (!candidate || typeof candidate !== "object") - return null; - const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : ""; - const issueNumber = candidate.issueNumber; - const title = typeof candidate.title === "string" ? candidate.title.trim() : ""; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - if (!Number.isInteger(issueNumber) || issueNumber <= 0 || !title) - return null; - const canonicalRepoFullName = `${owner}/${repo}`; - const labels = Array.isArray(candidate.labels) - ? candidate.labels - .filter((label) => typeof label === "string" && label.trim()) - .map((label) => label.trim()) - : []; - return { - owner, - repo, - repoFullName: canonicalRepoFullName, - issueNumber, - title, - labels, - commentsCount: Number.isFinite(candidate.commentsCount) ? candidate.commentsCount : 0, - createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : null, - updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null, - htmlUrl: typeof candidate.htmlUrl === "string" ? candidate.htmlUrl : null, - aiPolicyAllowed: candidate.aiPolicyAllowed !== false, - aiPolicySource: candidate.aiPolicySource === "AI-USAGE.md" || - candidate.aiPolicySource === "CONTRIBUTING.md" || - candidate.aiPolicySource === "none" - ? candidate.aiPolicySource - : "none", - }; -} -function buildGoalSpecsByRepo(options = {}) { - const goalSpecsByRepo = { ...(options.goalSpecsByRepo ?? {}) }; - const rawContentByRepo = options.goalSpecContentByRepo ?? {}; - for (const [repoFullName, content] of Object.entries(rawContentByRepo)) { - if (typeof content !== "string" || !content.trim()) - continue; - goalSpecsByRepo[repoFullName] = parseMinerGoalSpecContent(content).spec; - } - return goalSpecsByRepo; -} -function buildRankContext(options = {}) { - return { - nowMs: finiteEpochMs(options.nowMs), - highRiskDuplicateClusters: finiteNonNegativeInt(options.highRiskDuplicateClusters), - openPullRequests: finiteNonNegativeInt(options.openPullRequests), - goalSpecsByRepo: buildGoalSpecsByRepo(options), - }; -} -function collectCandidates(candidates) { - const input = Array.isArray(candidates) ? candidates : []; - let skippedInvalid = 0; - const normalized = []; - const seen = new Set(); - for (const candidate of input) { - const entry = normalizeCandidate(candidate); - if (!entry) { - skippedInvalid += 1; - continue; - } - const key = `${entry.repoFullName.toLowerCase()}#${entry.issueNumber}`; - if (seen.has(key)) - continue; - seen.add(key); - normalized.push(entry); - } - return { normalized, skippedInvalid }; -} -function rankedUsesDefaultGoalSpec(ranked, options = {}) { - const goalSpecsByRepo = buildGoalSpecsByRepo(options); - const specRepos = Object.keys(goalSpecsByRepo); - if (ranked.length === 0) - return specRepos.length === 0; - // The "ranked with the built-in default goal spec (no per-tenant .loopover-miner.yml supplied)" note is only - // truthful when the WHOLE batch fell back to the default -- so require EVERY ranked repo to lack a supplied spec, - // not just any one of them (#7226). With `.some`, a single spec-less repo made a mixed batch (where other repos - // genuinely had a spec supplied and applied) print the blanket note as if none did. - return ranked.every((issue) => { - const target = issue.repoFullName.trim().toLowerCase(); - return !specRepos.some((repo) => repo.trim().toLowerCase() === target); - }); -} -/** - * Rank metadata-only fan-out candidates locally. Never clones source, never uploads metadata, and never writes to - * GitHub — it only composes deterministic engine signals and returns the sorted list. - */ -export function rankCandidateIssues(candidates, options = {}) { - const { normalized } = collectCandidates(candidates); - return rankMetadataOpportunities(normalized, buildRankContext(options)); -} -export function rankCandidateIssuesWithSummary(candidates, options = {}) { - const { normalized, skippedInvalid } = collectCandidates(candidates); - const ranked = rankMetadataOpportunities(normalized, buildRankContext(options)); - return { - issues: ranked, - skippedInvalid, - usedDefaultGoalSpec: rankedUsesDefaultGoalSpec(ranked, options), - defaultGoalSpec: DEFAULT_MINER_GOAL_SPEC, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3Bwb3J0dW5pdHktcmFua2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsib3Bwb3J0dW5pdHktcmFua2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFDTCx1QkFBdUIsRUFDdkIseUJBQXlCLEVBQ3pCLHlCQUF5QixHQUMxQixNQUFNLGtCQUFrQixDQUFDO0FBNEIxQixTQUFTLGFBQWEsQ0FBQyxLQUFjO0lBQ25DLE9BQU8sTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUUsS0FBZ0IsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO0FBQ2pFLENBQUM7QUFFRCxTQUFTLG9CQUFvQixDQUFDLEtBQWM7SUFDMUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDO1FBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEMsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQWUsQ0FBQyxDQUFDLENBQUM7QUFDbEQsQ0FBQztBQUVELFNBQVMsa0JBQWtCLENBQUMsU0FBa0M7SUFDNUQsSUFBSSxDQUFDLFNBQVMsSUFBSSxPQUFPLFNBQVMsS0FBSyxRQUFRO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDN0QsTUFBTSxZQUFZLEdBQ2hCLE9BQU8sU0FBUyxDQUFDLFlBQVksS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNsRixNQUFNLFdBQVcsR0FBRyxTQUFTLENBQUMsV0FBcUIsQ0FBQztJQUNwRCxNQUFNLEtBQUssR0FBRyxPQUFPLFNBQVMsQ0FBQyxLQUFLLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDaEYsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNyRCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDeEQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLElBQUksV0FBVyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUs7UUFBRSxPQUFPLElBQUksQ0FBQztJQUM5RSxNQUFNLHFCQUFxQixHQUFHLEdBQUcsS0FBSyxJQUFJLElBQUksRUFBRSxDQUFDO0lBQ2pELE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQztRQUM1QyxDQUFDLENBQUMsU0FBUyxDQUFDLE1BQU07YUFDYixNQUFNLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7YUFDNUQsR0FBRyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDakMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNQLE9BQU87UUFDTCxLQUFLO1FBQ0wsSUFBSTtRQUNKLFlBQVksRUFBRSxxQkFBcUI7UUFDbkMsV0FBVztRQUNYLEtBQUs7UUFDTCxNQUFNO1FBQ04sYUFBYSxFQUFFLE1BQU0sQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsQ0FBRSxTQUFTLENBQUMsYUFBd0IsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNqRyxTQUFTLEVBQUUsT0FBTyxTQUFTLENBQUMsU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUMvRSxTQUFTLEVBQUUsT0FBTyxTQUFTLENBQUMsU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUMvRSxPQUFPLEVBQUUsT0FBTyxTQUFTLENBQUMsT0FBTyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUN6RSxlQUFlLEVBQUUsU0FBUyxDQUFDLGVBQWUsS0FBSyxLQUFLO1FBQ3BELGNBQWMsRUFDWixTQUFTLENBQUMsY0FBYyxLQUFLLGFBQWE7WUFDMUMsU0FBUyxDQUFDLGNBQWMsS0FBSyxpQkFBaUI7WUFDOUMsU0FBUyxDQUFDLGNBQWMsS0FBSyxNQUFNO1lBQ2pDLENBQUMsQ0FBQyxTQUFTLENBQUMsY0FBYztZQUMxQixDQUFDLENBQUMsTUFBTTtLQUNiLENBQUM7QUFDSixDQUFDO0FBSUQsU0FBUyxvQkFBb0IsQ0FBQyxVQUFzQyxFQUFFO0lBQ3BFLE1BQU0sZUFBZSxHQUFrQyxFQUFFLEdBQUcsQ0FBQyxPQUFPLENBQUMsZUFBZSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUM7SUFDOUYsTUFBTSxnQkFBZ0IsR0FBMkIsT0FBTyxDQUFDLHFCQUFxQixJQUFJLEVBQUUsQ0FBQztJQUNyRixLQUFLLE1BQU0sQ0FBQyxZQUFZLEVBQUUsT0FBTyxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxFQUFFLENBQUM7UUFDdkUsSUFBSSxPQUFPLE9BQU8sS0FBSyxRQUFRLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxFQUFFO1lBQUUsU0FBUztRQUM3RCxlQUFlLENBQUMsWUFBWSxDQUFDLEdBQUcseUJBQXlCLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDO0lBQzFFLENBQUM7SUFDRCxPQUFPLGVBQWUsQ0FBQztBQUN6QixDQUFDO0FBRUQsU0FBUyxnQkFBZ0IsQ0FBQyxVQUFzQyxFQUFFO0lBQ2hFLE9BQU87UUFDTCxLQUFLLEVBQUUsYUFBYSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7UUFDbkMseUJBQXlCLEVBQUUsb0JBQW9CLENBQUMsT0FBTyxDQUFDLHlCQUF5QixDQUFDO1FBQ2xGLGdCQUFnQixFQUFFLG9CQUFvQixDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQztRQUNoRSxlQUFlLEVBQUUsb0JBQW9CLENBQUMsT0FBTyxDQUFDO0tBQy9DLENBQUM7QUFDSixDQUFDO0FBRUQsU0FBUyxpQkFBaUIsQ0FBQyxVQUErQjtJQUN4RCxNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUMxRCxJQUFJLGNBQWMsR0FBRyxDQUFDLENBQUM7SUFDdkIsTUFBTSxVQUFVLEdBQTBCLEVBQUUsQ0FBQztJQUM3QyxNQUFNLElBQUksR0FBRyxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBQ3ZCLEtBQUssTUFBTSxTQUFTLElBQUksS0FBSyxFQUFFLENBQUM7UUFDOUIsTUFBTSxLQUFLLEdBQUcsa0JBQWtCLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDNUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ1gsY0FBYyxJQUFJLENBQUMsQ0FBQztZQUNwQixTQUFTO1FBQ1gsQ0FBQztRQUNELE1BQU0sR0FBRyxHQUFHLEdBQUcsS0FBSyxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsSUFBSSxLQUFLLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDdkUsSUFBSSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQztZQUFFLFNBQVM7UUFDNUIsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUNkLFVBQVUsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDekIsQ0FBQztJQUNELE9BQU8sRUFBRSxVQUFVLEVBQUUsY0FBYyxFQUFFLENBQUM7QUFDeEMsQ0FBQztBQUVELFNBQVMseUJBQXlCLENBQUMsTUFBOEIsRUFBRSxVQUFzQyxFQUFFO0lBQ3pHLE1BQU0sZUFBZSxHQUFHLG9CQUFvQixDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ3RELE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7SUFDL0MsSUFBSSxNQUFNLENBQUMsTUFBTSxLQUFLLENBQUM7UUFBRSxPQUFPLFNBQVMsQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUFDO0lBQ3ZELDZHQUE2RztJQUM3RyxrSEFBa0g7SUFDbEgsZ0hBQWdIO0lBQ2hILG9GQUFvRjtJQUNwRixPQUFPLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRTtRQUM1QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ3ZELE9BQU8sQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLEtBQUssTUFBTSxDQUFDLENBQUM7SUFDekUsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLG1CQUFtQixDQUNqQyxVQUErQixFQUMvQixVQUFzQyxFQUFFO0lBRXhDLE1BQU0sRUFBRSxVQUFVLEVBQUUsR0FBRyxpQkFBaUIsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNyRCxPQUFPLHlCQUF5QixDQUFDLFVBQVUsRUFBRSxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsQ0FBMkIsQ0FBQztBQUNwRyxDQUFDO0FBRUQsTUFBTSxVQUFVLDhCQUE4QixDQUM1QyxVQUErQixFQUMvQixVQUFzQyxFQUFFO0lBRXhDLE1BQU0sRUFBRSxVQUFVLEVBQUUsY0FBYyxFQUFFLEdBQUcsaUJBQWlCLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDckUsTUFBTSxNQUFNLEdBQUcseUJBQXlCLENBQUMsVUFBVSxFQUFFLGdCQUFnQixDQUFDLE9BQU8sQ0FBQyxDQUEyQixDQUFDO0lBQzFHLE9BQU87UUFDTCxNQUFNLEVBQUUsTUFBTTtRQUNkLGNBQWM7UUFDZCxtQkFBbUIsRUFBRSx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDO1FBQy9ELGVBQWUsRUFBRSx1QkFBdUI7S0FDekMsQ0FBQztBQUNKLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/orb-export.d.ts b/packages/loopover-miner/lib/orb-export.d.ts deleted file mode 100644 index e34de17e5b..0000000000 --- a/packages/loopover-miner/lib/orb-export.d.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { NormalizedPrOutcomePayload, PrOutcomeLedgerReader } from "./pr-outcome.js"; -/** OPT-IN: a laptop miner exports nothing unless a contributor explicitly turns it on. */ -export declare const ORB_EXPORT_ENABLED_BY_DEFAULT = false; -/** One anonymized outcome in an export batch — no raw repo name, PR number, or free-text reason. */ -export interface OrbExportRow { - repoHash: string; - prHash: string; - decision: string; - reasonBucket: string; - closedAt: string | null; -} -/** The local orb-export store: the per-instance anonymization secret + export cursor, in local SQLite. */ -export interface OrbExportStore { - dbPath: string; - getOrCreateAnonSecret(): string; - getCursor(): string | null; - setCursor(cursor: string): void; - close(): void; -} -/** Result of sending a batch to the AMS collector — `error` present only on a non-2xx response, a network - * failure, or an empty batch (never thrown). */ -export type AmsExportSendResult = { - sent: number; - error?: string; -}; -/** A pr_outcome record as produced by `readPrOutcomes` (the local ledger's latest-per-PR reduction). */ -export type OrbExportOutcome = NormalizedPrOutcomePayload & { - repoFullName: string; -}; -export declare function resolveOrbExportDbPath(env?: Record): string; -/** HMAC a value with the per-instance secret. Validates the secret (the shared engine primitive stays pure - * and doesn't), then delegates the actual hash to @loopover/engine's hmacAnonymize — the same primitive - * src/selfhost/orb-collector.ts uses, so both products anonymize identically. */ -export declare function hmacAnonymize(value: string | number, secret: string): string; -/** - * Turn the local pr_outcome map (pr-outcome.js `readPrOutcomes`) into an anonymized export batch: repo and PR - * identifiers are HMAC-hashed, and only the `decision` + a low-cardinality `reasonBucket` (already one of the - * miner's `REJECTION_REASONS`, else `"none"`) + `closedAt` leave. Pure and deterministic (rows sorted by prHash). - * Accepts either the Map `readPrOutcomes` returns or any iterable of outcome records. - */ -export declare function buildAnonymizedOrbBatch(outcomes: Iterable | Map, secret: string): OrbExportRow[]; -/** - * Open/create the local orb-export store: a small key/value SQLite table holding the per-instance anonymization - * secret and the export cursor. Mirrors the other miner ledgers' node:sqlite pattern — a `0o700` config dir and a - * `0o600` file, since the secret must never leave this machine. - */ -export declare function openOrbExportStore(dbPath?: string): OrbExportStore; -/** - * Collect the anonymized Orb export batch from the local pr_outcome ledger. OPT-IN: returns null (exports nothing) - * unless `enabled` is true — a third-party contributor's laptop must explicitly turn this on. Never performs the - * network POST itself; the caller sends the returned batch to the Orb ingest endpoint and then advances the store - * cursor, so this function stays pure over its inputs and the local store. - */ -export declare function collectOrbExportBatch(options?: { - store?: OrbExportStore; - eventLedger?: PrOutcomeLedgerReader; - enabled?: boolean; -}): OrbExportRow[] | null; -/** Stable per-instance identifier: a hash of the instance's own anon secret (no App-id concept on the AMS side, - * unlike orb-collector.ts's instanceId — a miner laptop has no GitHub App). */ -export declare function amsInstanceId(secret: string): string; -/** Drop rows already sent in a prior export: everything with a `closedAt` at/before the cursor. A row with no - * `closedAt` (shouldn't happen for a resolved PR, but defensive) is always included, since there is no - * watermark to compare it against. A null/unset cursor means "first export" — everything goes. */ -export declare function filterBatchSinceCursor(batch: OrbExportRow[], cursor: string | null): OrbExportRow[]; -/** The newest `closedAt` among a batch's rows, or `null` if none carry one — the next cursor value to persist - * after a successful send. */ -export declare function latestClosedAt(batch: OrbExportRow[]): string | null; -/** loopover's hosted AMS collector — mirrors orb-collector.ts's ORB_COLLECTOR_URL default pattern. */ -export declare const DEFAULT_AMS_COLLECTOR_URL = "https://api.loopover.ai/v1/ams/ingest"; -export declare function resolveAmsCollectorUrl(env?: Record): string; -/** - * POST an already-anonymized batch to the AMS ingest collector, signed the same way orb-collector.ts signs its - * own export (a full-length HMAC over the JSON body, distinct from the per-field hmacAnonymize truncated hash - * above — a body signature and a field anonymization hash are different concerns). Returns `{ sent }` on a 2xx - * response, `{ sent: 0, error }` otherwise — a network failure or non-2xx never throws, matching this module's - * fail-open posture (a telemetry hiccup must never break the miner's real work). - */ -export declare const DEFAULT_ORB_EXPORT_TIMEOUT_MS = 10000; -export declare function sendAmsExportBatch(options: { - batch: OrbExportRow[]; - secret: string; - collectorUrl?: string; - collectorToken?: string | undefined; - fetchFn?: typeof fetch; - timeoutMs?: number; -}): Promise; -export type ParsedOrbExportArgs = { - json: boolean; - enable: boolean; - send: boolean; - dryRun: boolean; -} | { - error: string; -}; -export declare function parseOrbExportArgs(args: string[]): ParsedOrbExportArgs; -/** CLI entry for the anonymized Orb telemetry batch-builder + sender (#4833 wired the caller-less exporter's - * batch-building; #5681 wired the network send). OPT-IN: prints nothing to export unless `--enable` is - * passed. `--enable` alone only builds+prints the anonymized batch locally — no network I/O, so a contributor - * can inspect exactly what would be sent first. `--enable --send` additionally POSTs the (cursor-filtered) - * batch to the AMS collector and advances the cursor on success, so a re-run doesn't resend history that was - * already delivered. */ -export declare function runOrbExportCli(args: string[], options?: { - openOrbExportStore?: () => OrbExportStore; - initEventLedger?: () => PrOutcomeLedgerReader; - sendAmsExportBatch?: (options: { - batch: OrbExportRow[]; - secret: string; - collectorToken?: string | undefined; - }) => Promise; - env?: Record; -}): Promise; diff --git a/packages/loopover-miner/lib/orb-export.js b/packages/loopover-miner/lib/orb-export.js deleted file mode 100644 index 6a54a47698..0000000000 --- a/packages/loopover-miner/lib/orb-export.js +++ /dev/null @@ -1,333 +0,0 @@ -import { chmodSync, mkdirSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; -import { DatabaseSync } from "node:sqlite"; -import { createHash, createHmac } from "node:crypto"; -import { generateAnonSecret, hmacAnonymize as engineHmacAnonymize } from "@loopover/engine"; -import { readPrOutcomes } from "./pr-outcome.js"; -import { initEventLedger } from "./event-ledger.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -// Optional anonymized Orb telemetry export (#4277, network send wired in #5681). The self-host Orb collector -// (src/selfhost/orb-collector.ts, #1255) is ALWAYS-ON for a maintainer's own instance; a miner runs on a -// third-party contributor's laptop with a much lower consent bar, so this export is OPT-IN (default OFF) — -// hence "optional". It mirrors the collector's privacy posture: repo/PR identifiers are HMAC-anonymized with a -// per-instance DEDICATED secret (generated once, persisted locally, single-purpose), and only a fixed -// low-cardinality reason bucket + the decision leave — never raw repo names or free text. The data source is -// the local pr_outcome ledger (pr-outcome.js), not a hosted D1. `generateAnonSecret`/`hmacAnonymize` are the -// same primitive src/selfhost/orb-collector.ts uses (@loopover/engine, #5680) — one anonymization -// implementation shared by both products instead of two independently-maintained copies. -/** OPT-IN: a laptop miner exports nothing unless a contributor explicitly turns it on. */ -export const ORB_EXPORT_ENABLED_BY_DEFAULT = false; -const ANON_SECRET_KEY = "anon_secret"; -const CURSOR_KEY = "export_cursor"; -const defaultDbFileName = "orb-export.sqlite3"; -export function resolveOrbExportDbPath(env = process.env) { - const explicitPath = typeof env.LOOPOVER_MINER_ORB_EXPORT_DB === "string" ? env.LOOPOVER_MINER_ORB_EXPORT_DB.trim() : ""; - if (explicitPath) - return explicitPath; - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" ? env.LOOPOVER_MINER_CONFIG_DIR.trim() : ""; - if (explicitConfigDir) - return join(explicitConfigDir, defaultDbFileName); - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultDbFileName); -} -// `dbPath` is always a real string here: this function's only caller (openOrbExportStore) already defaults its -// own parameter to resolveOrbExportDbPath() before ever reaching this call, so the nullish fallback historically -// here could never actually fire. -function normalizeDbPath(dbPath) { - const path = dbPath.trim(); - if (!path) - throw new Error("invalid_orb_export_db_path"); - return path; -} -/** HMAC a value with the per-instance secret. Validates the secret (the shared engine primitive stays pure - * and doesn't), then delegates the actual hash to @loopover/engine's hmacAnonymize — the same primitive - * src/selfhost/orb-collector.ts uses, so both products anonymize identically. */ -export function hmacAnonymize(value, secret) { - if (typeof secret !== "string" || !secret) - throw new Error("invalid_anon_secret"); - return engineHmacAnonymize(String(value), secret); -} -/** - * Turn the local pr_outcome map (pr-outcome.js `readPrOutcomes`) into an anonymized export batch: repo and PR - * identifiers are HMAC-hashed, and only the `decision` + a low-cardinality `reasonBucket` (already one of the - * miner's `REJECTION_REASONS`, else `"none"`) + `closedAt` leave. Pure and deterministic (rows sorted by prHash). - * Accepts either the Map `readPrOutcomes` returns or any iterable of outcome records. - */ -export function buildAnonymizedOrbBatch(outcomes, secret) { - // `outcomes` is never nullish here (the parameter type guarantees a real Map or Iterable), so the historical - // `outcomes &&` truthiness guard and the loop's `?? []` fallback could never actually fire; both are dropped. - // Whichever branch runs, the elements are OrbExportOutcome: a Map's .values() yields them directly, and any - // non-Map input is one by this function's own contract -- iterating a Map's entries() shape (`[key, value]`) - // is not actually reachable here, since every real Map has a `.values` method. - const iterable = (typeof outcomes.values === "function" - ? outcomes.values() - : outcomes); - const rows = []; - for (const outcome of iterable) { - if (!outcome || typeof outcome.repoFullName !== "string" || !outcome.repoFullName.trim()) - continue; - if (!Number.isInteger(outcome.prNumber) || outcome.prNumber <= 0) - continue; - rows.push({ - repoHash: hmacAnonymize(outcome.repoFullName, secret), - prHash: hmacAnonymize(`${outcome.repoFullName}:${outcome.prNumber}`, secret), - decision: outcome.decision, - reasonBucket: typeof outcome.reason === "string" && outcome.reason ? outcome.reason : "none", - closedAt: typeof outcome.closedAt === "string" && outcome.closedAt ? outcome.closedAt : null, - }); - } - rows.sort((a, b) => a.prHash.localeCompare(b.prHash)); - return rows; -} -/** - * Open/create the local orb-export store: a small key/value SQLite table holding the per-instance anonymization - * secret and the export cursor. Mirrors the other miner ledgers' node:sqlite pattern — a `0o700` config dir and a - * `0o600` file, since the secret must never leave this machine. - */ -export function openOrbExportStore(dbPath = resolveOrbExportDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - mkdirSync(dirname(resolvedPath), { recursive: true, mode: 0o700 }); - const db = new DatabaseSync(resolvedPath); - chmodSync(resolvedPath, 0o600); - db.exec("PRAGMA busy_timeout = 5000"); - db.exec(`CREATE TABLE IF NOT EXISTS orb_export_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`); - const getStatement = db.prepare("SELECT value FROM orb_export_meta WHERE key = ?"); - const setStatement = db.prepare("INSERT INTO orb_export_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"); - const readValue = (key) => { - const row = getStatement.get(key); - return row && typeof row.value === "string" ? row.value : null; - }; - return { - dbPath: resolvedPath, - /** The per-instance DEDICATED anonymization secret — generated once (256-bit) and persisted, then reused - * forever so a repo/PR always hashes the same way. Single-purpose: only this export uses it. */ - getOrCreateAnonSecret() { - const existing = readValue(ANON_SECRET_KEY); - if (existing) - return existing; - const generated = generateAnonSecret(); - setStatement.run(ANON_SECRET_KEY, generated); - return generated; - }, - /** The export watermark (opaque string), or null before the first export. */ - getCursor() { - return readValue(CURSOR_KEY); - }, - setCursor(cursor) { - setStatement.run(CURSOR_KEY, String(cursor)); - }, - close() { - db.close(); - }, - }; -} -/** - * Collect the anonymized Orb export batch from the local pr_outcome ledger. OPT-IN: returns null (exports nothing) - * unless `enabled` is true — a third-party contributor's laptop must explicitly turn this on. Never performs the - * network POST itself; the caller sends the returned batch to the Orb ingest endpoint and then advances the store - * cursor, so this function stays pure over its inputs and the local store. - */ -export function collectOrbExportBatch( -// store/eventLedger are optional here (unlike the wider public contract in orb-export.d.ts) because the real -// callers below can omit both entirely -- `enabled` defaults to false, so the function returns null before -// either is ever touched, and only requires them once a caller explicitly opts in. -options = {}) { - const { store, eventLedger, enabled = ORB_EXPORT_ENABLED_BY_DEFAULT } = options; - if (!enabled) - return null; - if (!store || typeof store.getOrCreateAnonSecret !== "function") - throw new Error("invalid_orb_export_store"); - // No guard on eventLedger here, matching the original: an enabled call without one fails inside - // readPrOutcomes itself rather than being pre-validated (only `store` gets a fail-loud check above). - const outcomes = readPrOutcomes(eventLedger); - return buildAnonymizedOrbBatch(outcomes, store.getOrCreateAnonSecret()); -} -/** Stable per-instance identifier: a hash of the instance's own anon secret (no App-id concept on the AMS side, - * unlike orb-collector.ts's instanceId — a miner laptop has no GitHub App). */ -export function amsInstanceId(secret) { - return createHash("sha256").update(String(secret)).digest("hex").slice(0, 16); -} -/** Drop rows already sent in a prior export: everything with a `closedAt` at/before the cursor. A row with no - * `closedAt` (shouldn't happen for a resolved PR, but defensive) is always included, since there is no - * watermark to compare it against. A null/unset cursor means "first export" — everything goes. */ -export function filterBatchSinceCursor(batch, cursor) { - if (!cursor) - return batch; - return batch.filter((row) => !row.closedAt || row.closedAt > cursor); -} -/** The newest `closedAt` among a batch's rows, or `null` if none carry one — the next cursor value to persist - * after a successful send. */ -export function latestClosedAt(batch) { - let latest = null; - for (const row of batch) { - if (row.closedAt && (latest === null || row.closedAt > latest)) - latest = row.closedAt; - } - return latest; -} -/** loopover's hosted AMS collector — mirrors orb-collector.ts's ORB_COLLECTOR_URL default pattern. */ -export const DEFAULT_AMS_COLLECTOR_URL = "https://api.loopover.ai/v1/ams/ingest"; -export function resolveAmsCollectorUrl(env = process.env) { - const explicit = typeof env.LOOPOVER_MINER_AMS_COLLECTOR_URL === "string" ? env.LOOPOVER_MINER_AMS_COLLECTOR_URL.trim() : ""; - return explicit || DEFAULT_AMS_COLLECTOR_URL; -} -/** - * POST an already-anonymized batch to the AMS ingest collector, signed the same way orb-collector.ts signs its - * own export (a full-length HMAC over the JSON body, distinct from the per-field hmacAnonymize truncated hash - * above — a body signature and a field anonymization hash are different concerns). Returns `{ sent }` on a 2xx - * response, `{ sent: 0, error }` otherwise — a network failure or non-2xx never throws, matching this module's - * fail-open posture (a telemetry hiccup must never break the miner's real work). - */ -// Bound a single AMS-collector POST so a hung/black-holed collector can't stall the export indefinitely (#7237). -// 10s matches this package's other default request timeouts (live-issue-snapshot.js / opportunity-fanout.js). -export const DEFAULT_ORB_EXPORT_TIMEOUT_MS = 10_000; -export async function sendAmsExportBatch(options) { - const { batch, secret, collectorUrl = resolveAmsCollectorUrl(), collectorToken, fetchFn = fetch, timeoutMs = DEFAULT_ORB_EXPORT_TIMEOUT_MS, } = options; - if (!Array.isArray(batch) || batch.length === 0) - return { sent: 0 }; - const instanceId = amsInstanceId(secret); - const body = JSON.stringify({ instanceId, events: batch }); - const signature = createHmac("sha256", secret).update(body).digest("hex"); - try { - const res = await fetchFn(collectorUrl, { - method: "POST", - headers: { - "content-type": "application/json", - "x-ams-signature": `sha256=${signature}`, - "x-ams-instance": instanceId, - ...(collectorToken ? { authorization: `Bearer ${collectorToken}` } : {}), - }, - body, - signal: AbortSignal.timeout(timeoutMs), - }); - if (!res.ok) - return { sent: 0, error: `http_${res.status}` }; - } - catch (error) { - return { sent: 0, error: describeCliError(error) }; - } - return { sent: batch.length }; -} -const ORB_EXPORT_USAGE = "Usage: loopover-miner orb export [--enable] [--send] [--dry-run] [--json]"; -export function parseOrbExportArgs(args) { - const options = { json: false, enable: false, send: false, dryRun: false }; - for (const token of args) { - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--enable") { - options.enable = true; - continue; - } - // Distinct from --enable: --enable alone only builds+prints the anonymized batch locally (no network I/O), - // so a contributor can inspect exactly what would be sent before ever transmitting it. --send additionally - // POSTs that batch to the collector and advances the cursor — the previously-missing network step (#5681). - if (token === "--send") { - options.send = true; - continue; - } - // #4847: openOrbExportStore() itself creates the local SQLite file (a real write) even before any secret is - // generated, so a dry run reports what would happen and returns before opening any store at all. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - return { error: ORB_EXPORT_USAGE }; - } - return options; -} -/** CLI entry for the anonymized Orb telemetry batch-builder + sender (#4833 wired the caller-less exporter's - * batch-building; #5681 wired the network send). OPT-IN: prints nothing to export unless `--enable` is - * passed. `--enable` alone only builds+prints the anonymized batch locally — no network I/O, so a contributor - * can inspect exactly what would be sent first. `--enable --send` additionally POSTs the (cursor-filtered) - * batch to the AMS collector and advances the cursor on success, so a re-run doesn't resend history that was - * already delivered. */ -export async function runOrbExportCli(args, options = {}) { - const parsed = parseOrbExportArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", enabled: parsed.enable, send: parsed.send }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else if (parsed.enable && parsed.send) { - console.log("DRY RUN: would build an anonymized Orb export batch and send it to the collector. No local writes or network calls were made."); - } - else if (parsed.enable) { - console.log("DRY RUN: would build and report an anonymized Orb export batch. No local writes were made."); - } - else { - console.log("DRY RUN: orb export is opt-in and disabled — pass --enable to build an anonymized batch. No local writes were made."); - } - return 0; - } - // Open the stores INSIDE the try so a bad config path / SQLite open failure returns 2 instead of crashing the - // process; the finally guards each close with `?.` since either initializer may have thrown before assigning. - // The --send path's await happens INSIDE this try so `finally` (which closes the store) can never run before - // the cursor advance below it -- resolving the send result AFTER the store closed would write to a dead handle. - const ownsStore = options.openOrbExportStore === undefined; - const ownsLedger = options.initEventLedger === undefined; - let store; - let eventLedger; - try { - store = (options.openOrbExportStore ?? openOrbExportStore)(); - eventLedger = (options.initEventLedger ?? initEventLedger)(); - const batch = collectOrbExportBatch({ store, eventLedger, enabled: parsed.enable }); - if (batch === null) { - if (parsed.json) - console.log(JSON.stringify({ enabled: false, batch: null }, null, 2)); - else - console.log("orb export is opt-in and disabled — pass --enable to build an anonymized batch"); - return 0; - } - if (!parsed.send) { - if (parsed.json) - console.log(JSON.stringify({ enabled: true, sent: false, batch }, null, 2)); - else - console.log(`${batch.length} anonymized event(s) — pass --send to transmit them to the collector`); - return 0; - } - const cursor = store.getCursor(); - const toSend = filterBatchSinceCursor(batch, cursor); - if (toSend.length === 0) { - if (parsed.json) - console.log(JSON.stringify({ enabled: true, sent: 0, skipped: batch.length }, null, 2)); - else - console.log("no new events since the last export"); - return 0; - } - const send = options.sendAmsExportBatch ?? sendAmsExportBatch; - const secret = store.getOrCreateAnonSecret(); - const env = options.env ?? process.env; - const collectorToken = env.LOOPOVER_MINER_AMS_COLLECTOR_TOKEN ?? ""; - const sendResult = await send({ batch: toSend, secret, collectorToken }); - if (sendResult.sent > 0) { - const nextCursor = latestClosedAt(toSend); - if (nextCursor) - store.setCursor(nextCursor); - } - if (parsed.json) - console.log(JSON.stringify({ enabled: true, ...sendResult, skipped: batch.length - toSend.length }, null, 2)); - else if (sendResult.error) - console.log(`export failed: ${sendResult.error}`); - else - console.log(`sent ${sendResult.sent} anonymized event(s)`); - return sendResult.error ? 1 : 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - if (ownsStore) - store?.close(); - if (ownsLedger) - eventLedger?.close?.(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3JiLWV4cG9ydC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIm9yYi1leHBvcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDL0MsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNsQyxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzNDLE9BQU8sRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ3JELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxhQUFhLElBQUksbUJBQW1CLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM1RixPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFakQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3BELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRiw2R0FBNkc7QUFDN0cseUdBQXlHO0FBQ3pHLDJHQUEyRztBQUMzRywrR0FBK0c7QUFDL0csc0dBQXNHO0FBQ3RHLDZHQUE2RztBQUM3Ryw2R0FBNkc7QUFDN0csa0dBQWtHO0FBQ2xHLHlGQUF5RjtBQUV6RiwwRkFBMEY7QUFDMUYsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsS0FBSyxDQUFDO0FBMkJuRCxNQUFNLGVBQWUsR0FBRyxhQUFhLENBQUM7QUFDdEMsTUFBTSxVQUFVLEdBQUcsZUFBZSxDQUFDO0FBQ25DLE1BQU0saUJBQWlCLEdBQUcsb0JBQW9CLENBQUM7QUFFL0MsTUFBTSxVQUFVLHNCQUFzQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQzFGLE1BQU0sWUFBWSxHQUNoQixP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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/plan-store-cli.d.ts b/packages/loopover-miner/lib/plan-store-cli.d.ts deleted file mode 100644 index c64d6f7e5d..0000000000 --- a/packages/loopover-miner/lib/plan-store-cli.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PlanRecord, PlanStatus, PlanStore } from "./plan-store.js"; -export type ParsedPlanListArgs = { - json: boolean; - status: PlanStatus | null; -} | { - error: string; -}; -export type ParsedPlanShowArgs = { - planId: string; - json: boolean; -} | { - error: string; -}; -export type PlanCliOptions = { - openPlanStore?: () => PlanStore; -}; -export declare function parsePlanListArgs(args: string[]): ParsedPlanListArgs; -export declare function parsePlanShowArgs(args: string[]): ParsedPlanShowArgs; -export declare function renderPlanTable(plans: PlanRecord[]): string; -export declare function runPlanList(args: string[], options?: PlanCliOptions): number; -export declare function runPlanShow(args: string[], options?: PlanCliOptions): number; -export declare function runPlanCli(subcommand: string | undefined, args: string[], options?: PlanCliOptions): number; diff --git a/packages/loopover-miner/lib/plan-store-cli.js b/packages/loopover-miner/lib/plan-store-cli.js deleted file mode 100644 index b271ade58e..0000000000 --- a/packages/loopover-miner/lib/plan-store-cli.js +++ /dev/null @@ -1,147 +0,0 @@ -import { PLAN_STATUSES, openPlanStore } from "./plan-store.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -const PLAN_LIST_USAGE = "Usage: loopover-miner plan list [--status pending|running|completed|failed] [--json]"; -const PLAN_SHOW_USAGE = "Usage: loopover-miner plan show [--json]"; -function parseJsonFlag(args) { - const options = { json: false }; - const positional = []; - for (const token of args) { - if (token === "--json") { - options.json = true; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - return { positional, ...options }; -} -export function parsePlanListArgs(args) { - const options = { json: false, status: null }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--status") { - const status = args[index + 1]; - if (!status || status.startsWith("-")) - return { error: PLAN_LIST_USAGE }; - if (!PLAN_STATUSES.includes(status)) { - return { error: `Invalid status: ${status}. Expected one of ${PLAN_STATUSES.join(", ")}.` }; - } - options.status = status; - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - positional.push(token); - } - if (positional.length > 0) - return { error: PLAN_LIST_USAGE }; - return options; -} -export function parsePlanShowArgs(args) { - const parsed = parseJsonFlag(args); - if ("error" in parsed) - return parsed; - if (parsed.positional.length !== 1) - return { error: PLAN_SHOW_USAGE }; - const planId = parsed.positional[0]?.trim(); - if (!planId) - return { error: PLAN_SHOW_USAGE }; - return { - planId, - json: parsed.json, - }; -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderPlanTable(plans) { - if (!Array.isArray(plans) || plans.length === 0) - return "no saved plans"; - const header = [ - "plan-id".padEnd(20), - "status".padEnd(10), - "steps".padStart(5), - "updated-at".padEnd(24), - ].join(" "); - const lines = plans.map((record) => [ - record.planId.padEnd(20), - record.status.padEnd(10), - String(record.plan.steps.length).padStart(5), - display(record.updatedAt).padEnd(24), - ].join(" ")); - return [header, ...lines].join("\n"); -} -function withPlanStore(options, run) { - const ownsStore = options.openPlanStore === undefined; - const planStore = (options.openPlanStore ?? openPlanStore)(); - try { - return run(planStore); - } - finally { - if (ownsStore) - planStore.close(); - } -} -export function runPlanList(args, options = {}) { - const parsed = parsePlanListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return withPlanStore(options, (planStore) => { - const plans = planStore.listPlans({ status: parsed.status }); - if (parsed.json) { - console.log(JSON.stringify({ plans }, null, 2)); - } - else { - console.log(renderPlanTable(plans)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runPlanShow(args, options = {}) { - const parsed = parsePlanShowArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return withPlanStore(options, (planStore) => { - const plan = planStore.loadPlan(parsed.planId); - if (!plan) { - return reportCliFailure(parsed.json, "plan_not_found"); - } - if (parsed.json) { - console.log(JSON.stringify({ plan }, null, 2)); - } - else { - console.log(`${plan.status} (${plan.plan.steps.length} steps)`); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runPlanCli(subcommand, args, options = {}) { - if (subcommand === "list") - return runPlanList(args, options); - if (subcommand === "show") - return runPlanShow(args, options); - return reportCliFailure(argsWantJson(args), `Unknown plan subcommand: ${subcommand ?? ""}. ${PLAN_LIST_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGxhbi1zdG9yZS1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwbGFuLXN0b3JlLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsYUFBYSxFQUFFLGFBQWEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRS9ELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRixNQUFNLGVBQWUsR0FDbkIsc0ZBQXNGLENBQUM7QUFDekYsTUFBTSxlQUFlLEdBQUcsbURBQW1ELENBQUM7QUFvQjVFLFNBQVMsYUFBYSxDQUFDLElBQWM7SUFDbkMsTUFBTSxPQUFPLEdBQUcsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDaEMsTUFBTSxVQUFVLEdBQWEsRUFBRSxDQUFDO0lBRWhDLEtBQUssTUFBTSxLQUFLLElBQUksSUFBSSxFQUFFLENBQUM7UUFDekIsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7WUFDcEIsU0FBUztRQUNYLENBQUM7UUFDRCxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUMxQixPQUFPLEVBQUUsS0FBSyxFQUFFLG1CQUFtQixLQUFLLEVBQUUsRUFBRSxDQUFDO1FBQy9DLENBQUM7UUFDRCxVQUFVLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3pCLENBQUM7SUFFRCxPQUFPLEVBQUUsVUFBVSxFQUFFLEdBQUcsT0FBTyxFQUFFLENBQUM7QUFDcEMsQ0FBQztBQUVELE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxJQUFjO0lBQzlDLE1BQU0sT0FBTyxHQUFpRCxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDO0lBQzVGLE1BQU0sVUFBVSxHQUFhLEVBQUUsQ0FBQztJQUVo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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/plan-store.d.ts b/packages/loopover-miner/lib/plan-store.d.ts deleted file mode 100644 index 13d9fdc804..0000000000 --- a/packages/loopover-miner/lib/plan-store.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type PlanStepStatus = "pending" | "running" | "completed" | "failed" | "skipped"; -export type PlanStep = { - id: string; - title: string; - actionClass?: string; - dependsOn: string[]; - status: PlanStepStatus; - attempts: number; - maxAttempts: number; - lastError?: string | null; -}; -export type PlanDag = { - steps: PlanStep[]; -}; -export type PlanStatus = "pending" | "running" | "completed" | "failed"; -export type PlanRecord = { - planId: string; - plan: PlanDag; - status: PlanStatus; - updatedAt: string; -}; -export type ListPlansFilter = { - status?: PlanStatus | null; -}; -export type PlanStore = { - dbPath: string; - savePlan(planId: string, plan: PlanDag): PlanRecord; - loadPlan(planId: string): PlanRecord | null; - listPlans(filter?: ListPlansFilter): PlanRecord[]; - close(): void; -}; -/** Derived plan-level status used for `listPlans({ status })`. */ -export declare const PLAN_STATUSES: readonly PlanStatus[]; -export declare function resolvePlanStoreDbPath(env?: Record): string; -/** - * Opens the local plan store, creating the table on first use. `savePlan` is a single atomic INSERT…ON CONFLICT - * upsert keyed by `plan_id`; the plan JSON is validated on save AND re-validated on load, so a corrupted row is - * rejected rather than silently returned. (#2318) - */ -export declare function openPlanStore(dbPath?: string): PlanStore; -export declare function savePlan(planId: string, plan: PlanDag): PlanRecord; -export declare function loadPlan(planId: string): PlanRecord | null; -export declare function listPlans(filter?: ListPlansFilter): PlanRecord[]; -export declare function closeDefaultPlanStore(): void; diff --git a/packages/loopover-miner/lib/plan-store.js b/packages/loopover-miner/lib/plan-store.js deleted file mode 100644 index e5aca1dc86..0000000000 --- a/packages/loopover-miner/lib/plan-store.js +++ /dev/null @@ -1,232 +0,0 @@ -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -const PLAN_STEP_STATUSES = Object.freeze([ - "pending", - "running", - "completed", - "failed", - "skipped", -]); -/** Derived plan-level status used for `listPlans({ status })`. */ -export const PLAN_STATUSES = Object.freeze(["pending", "running", "completed", "failed"]); -const stepStatusSet = new Set(PLAN_STEP_STATUSES); -const planStatusSet = new Set(PLAN_STATUSES); -const defaultDbFileName = "plan-store.sqlite3"; -let defaultPlanStore = null; -export function resolvePlanStoreDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_PLAN_STORE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolvePlanStoreDbPath(), "invalid_plan_store_db_path"); -} -function normalizePlanId(planId) { - if (typeof planId !== "string" || !planId.trim()) - throw new Error("invalid_plan_id"); - return planId.trim(); -} -function normalizePlanStatusFilter(status) { - if (status === undefined || status === null) - return undefined; - if (!planStatusSet.has(status)) - throw new Error("invalid_status"); - return status; -} -function isBoundedString(value, min, max) { - return typeof value === "string" && value.length >= min && value.length <= max; -} -function isBoundedInt(value, min, max) { - return Number.isInteger(value) && value >= min && value <= max; -} -const STEP_KEYS = new Set(["id", "title", "actionClass", "dependsOn", "status", "attempts", "maxAttempts", "lastError"]); -function isValidStep(step) { - if (!step || typeof step !== "object" || Array.isArray(step)) - return false; - for (const key of Object.keys(step)) - if (!STEP_KEYS.has(key)) - return false; // strict: no unknown keys - const candidate = step; - if (!isBoundedString(candidate.id, 1, 100) || !isBoundedString(candidate.title, 1, 300)) - return false; - if (candidate.actionClass !== undefined && !isBoundedString(candidate.actionClass, 1, 60)) - return false; - if (!Array.isArray(candidate.dependsOn) || candidate.dependsOn.length > 50) - return false; - if (!candidate.dependsOn.every((dep) => isBoundedString(dep, 1, 100))) - return false; - if (!stepStatusSet.has(candidate.status)) - return false; - if (!isBoundedInt(candidate.attempts, 0, Number.MAX_SAFE_INTEGER)) - return false; - if (!isBoundedInt(candidate.maxAttempts, 1, 10)) - return false; - if (candidate.lastError !== undefined - && candidate.lastError !== null - && !isBoundedString(candidate.lastError, 0, 2000)) { - return false; - } - return true; -} -/** Validate a plan against the `planDagSchema` shape (strict `{ steps: PlanStep[] }`, ≤100 steps). Throws on any - * malformed field so a bad plan can neither be saved nor read back. */ -function validatePlanDag(plan) { - if (!plan || typeof plan !== "object" || Array.isArray(plan)) - throw new Error("invalid_plan"); - const keys = Object.keys(plan); - if (keys.length !== 1 || keys[0] !== "steps") - throw new Error("invalid_plan"); - const candidate = plan; - if (!Array.isArray(candidate.steps) || candidate.steps.length === 0 || candidate.steps.length > 100) { - throw new Error("invalid_plan"); - } - if (!candidate.steps.every(isValidStep)) - throw new Error("invalid_plan"); - const steps = candidate.steps; - const seenStepIds = new Set(); - for (const step of steps) { - if (seenStepIds.has(step.id)) - throw new Error("invalid_plan"); - seenStepIds.add(step.id); - } - for (const step of steps) { - for (const dep of step.dependsOn) { - if (dep === step.id || !seenStepIds.has(dep)) - throw new Error("invalid_plan"); - } - } - const color = new Map(); - const byId = new Map(steps.map((step) => [step.id, step])); - const hasCycle = (id) => { - color.set(id, 1); - // Non-null: every `id` this is called with is either a step's own id (inserted into `byId` above) or a - // `dep` whose `byId.has(dep)` was just confirmed true by the caller -- `byId.get(id)` can never miss here. - for (const dep of byId.get(id).dependsOn) { - const depColor = color.get(dep) ?? 0; - if (depColor === 1) - return true; - if (depColor === 0 && byId.has(dep) && hasCycle(dep)) - return true; - } - color.set(id, 2); - return false; - }; - for (const step of steps) { - if ((color.get(step.id) ?? 0) === 0 && hasCycle(step.id)) { - throw new Error("invalid_plan"); - } - } - return { steps }; -} -/** Derive a plan-level status from its steps: any failed → failed; else any running → running; else all steps - * finished (completed/skipped) with at least one step → completed; otherwise pending. */ -function computePlanStatus(plan) { - const steps = plan.steps; - if (steps.some((step) => step.status === "failed")) - return "failed"; - if (steps.some((step) => step.status === "running")) - return "running"; - if (steps.length > 0 && steps.every((step) => step.status === "completed" || step.status === "skipped")) { - return "completed"; - } - return "pending"; -} -function rowToRecord(row) { - let plan; - try { - plan = validatePlanDag(JSON.parse(row.plan_json)); - } - catch { - throw new Error("corrupted_plan_row"); // stored blob no longer matches the plan shape - } - // Also fail closed on the status column: a manually-edited or legacy row (predating the CHECK constraint) could - // hold a status outside PLAN_STATUSES, which would otherwise violate the exported PlanRecord contract on read. - if (!planStatusSet.has(row.status)) - throw new Error("corrupted_plan_row"); - return { planId: row.plan_id, plan, status: row.status, updatedAt: row.updated_at }; -} -// v1 -> v2 (#4939/#6597): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of -// this same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing -// reads or writes it yet. Same defensive column-presence guard as this file's sibling stores' own additive -// migrations (e.g. event-ledger.js's addTenantIdColumn). -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(miner_plans)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE miner_plans ADD COLUMN tenant_id TEXT"); -} -/** - * Opens the local plan store, creating the table on first use. `savePlan` is a single atomic INSERT…ON CONFLICT - * upsert keyed by `plan_id`; the plan JSON is validated on save AND re-validated on load, so a corrupted row is - * rejected rather than silently returned. (#2318) - */ -export function openPlanStore(dbPath = resolvePlanStoreDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - // openLocalStoreDb centralizes the mkdir(0o700)/chmod(0o600)/busy_timeout + crash-safe cleanup registration and - // treats ':memory:' as a no-file special case, so this store no longer hand-rolls that boilerplate (#4826). - const db = openLocalStoreDb(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS miner_plans ( - plan_id TEXT PRIMARY KEY, - plan_json TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed')), - updated_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addTenantIdColumn]); - const saveStatement = db.prepare(` - INSERT INTO miner_plans (plan_id, plan_json, status, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(plan_id) DO UPDATE SET - plan_json = excluded.plan_json, - status = excluded.status, - updated_at = excluded.updated_at - `); - const getStatement = db.prepare("SELECT * FROM miner_plans WHERE plan_id = ?"); - const listAllStatement = db.prepare("SELECT * FROM miner_plans ORDER BY plan_id ASC"); - const listStatusStatement = db.prepare("SELECT * FROM miner_plans WHERE status = ? ORDER BY plan_id ASC"); - return { - dbPath: resolvedPath, - savePlan(planId, plan) { - const id = normalizePlanId(planId); - validatePlanDag(plan); - const status = computePlanStatus(plan); - const updatedAt = new Date().toISOString(); - saveStatement.run(id, JSON.stringify(plan), status, updatedAt); - return { planId: id, plan, status, updatedAt }; - }, - loadPlan(planId) { - const row = getStatement.get(normalizePlanId(planId)); - return row ? rowToRecord(row) : null; - }, - listPlans(filter = {}) { - const status = normalizePlanStatusFilter(filter.status); - const rows = (status !== undefined ? listStatusStatement.all(status) : listAllStatement.all()); - return rows.map(rowToRecord); - }, - close() { - db.close(); - }, - }; -} -function getDefaultPlanStore() { - defaultPlanStore ??= openPlanStore(); - return defaultPlanStore; -} -export function savePlan(planId, plan) { - return getDefaultPlanStore().savePlan(planId, plan); -} -export function loadPlan(planId) { - return getDefaultPlanStore().loadPlan(planId); -} -export function listPlans(filter) { - return getDefaultPlanStore().listPlans(filter); -} -export function closeDefaultPlanStore() { - if (!defaultPlanStore) - return; - defaultPlanStore.close(); - defaultPlanStore = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGxhbi1zdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBsYW4tc3RvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLHlCQUF5QixFQUFFLGdCQUFnQixFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDeEcsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUErQzVELE1BQU0sa0JBQWtCLEdBQThCLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbEUsU0FBUztJQUNULFNBQVM7SUFDVCxXQUFXO0lBQ1gsUUFBUTtJQUNSLFNBQVM7Q0FDVixDQUFDLENBQUM7QUFDSCxrRUFBa0U7QUFDbEUsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUEwQixNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsU0FBUyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztBQUVqSCxNQUFNLGFBQWEsR0FBZ0IsSUFBSSxHQUFHLENBQUMsa0JBQWtCLENBQUMsQ0FBQztBQUMvRCxNQUFNLGFBQWEsR0FBZ0IsSUFBSSxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7QUFDMUQsTUFBTSxpQkFBaUIsR0FBRyxvQkFBb0IsQ0FBQztBQUMvQyxJQUFJLGdCQUFnQixHQUFxQixJQUFJLENBQUM7QUFFOUMsTUFBTSxVQUFVLHNCQUFzQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQzFGLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsOEJBQThCLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDekYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWlDO0lBQ3hELE9BQU8seUJBQXlCLENBQUMsTUFBTSxFQUFFLHNCQUFzQixFQUFFLEVBQUUsNEJBQTRCLENBQUMsQ0FBQztBQUNuRyxDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsTUFBZTtJQUN0QyxJQUFJLE9BQU8sTUFBTSxLQUFLLFFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/policy-doc-cache.d.ts b/packages/loopover-miner/lib/policy-doc-cache.d.ts deleted file mode 100644 index a90dddb97a..0000000000 --- a/packages/loopover-miner/lib/policy-doc-cache.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type PolicyDocCacheEntry = { - etag: string; - content: string; -}; -export type PolicyDocCacheWrite = { - url: string; - etag: string; - content: string; - updatedAt: string; -}; -export type PolicyDocCacheStore = { - dbPath: string; - get(url: string): PolicyDocCacheEntry | null; - put(url: string, etag: string, content: string): PolicyDocCacheWrite; - close(): void; -}; -/** The read/write surface opportunity-fanout.js needs to inject a cache without depending on the SQLite store. */ -export type PolicyDocCache = Pick; -export declare function resolvePolicyDocCacheDbPath(env?: Record): string; -/** - * Opens the 100% local/client-side miner policy-doc ETag cache. The database only lives on this machine; this - * module never uploads, syncs, or phones home with its contents. (#4842) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations still use the underlying DatabaseSync until those helpers are migrated. - * Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export declare function initPolicyDocCacheStore(dbPath?: string): PolicyDocCacheStore; diff --git a/packages/loopover-miner/lib/policy-doc-cache.js b/packages/loopover-miner/lib/policy-doc-cache.js deleted file mode 100644 index 49bb1ce9e3..0000000000 --- a/packages/loopover-miner/lib/policy-doc-cache.js +++ /dev/null @@ -1,81 +0,0 @@ -import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -// Local ETag cache for discovery's small policy-doc fetches (#4842). `discover` refetches each target repo's -// AI-USAGE.md/CONTRIBUTING.md on every run even though they rarely change, spending rate-limit budget on static -// content; this store lets opportunity-fanout.js revalidate with a conditional GET (If-None-Match) instead, and -// GitHub answers an unchanged doc with a 304 that costs no primary rate-limit budget. A 304 is a GitHub-confirmed -// unchanged body -- the cached content is only ever served AFTER a same-run revalidation, never blindly -- so this -// can never surface a stale policy that would wrongly permit autonomous work on an opted-out repo. Same 100% -// local/client-side discipline (mirrors run-state.js and the other stores this package owns via local-store.js): -// the file lives only on this machine and is never uploaded, synced, or phoned home with. -const defaultDbFileName = "policy-doc-cache.sqlite3"; -export function resolvePolicyDocCacheDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_POLICY_DOC_CACHE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolvePolicyDocCacheDbPath(), "invalid_policy_doc_cache_db_path"); -} -function normalizeUrl(url) { - if (typeof url !== "string") - throw new Error("invalid_policy_doc_url"); - const trimmed = url.trim(); - if (!trimmed) - throw new Error("invalid_policy_doc_url"); - return trimmed; -} -/** - * Opens the 100% local/client-side miner policy-doc ETag cache. The database only lives on this machine; this - * module never uploads, syncs, or phones home with its contents. (#4842) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations still use the underlying DatabaseSync until those helpers are migrated. - * Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export function initPolicyDocCacheStore(dbPath = resolvePolicyDocCacheDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS policy_doc_cache ( - url TEXT PRIMARY KEY, - etag TEXT NOT NULL, - content TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations (none yet). - applySchemaMigrations(db, []); - const getSql = "SELECT etag, content FROM policy_doc_cache WHERE url = ?"; - const putSql = ` - INSERT INTO policy_doc_cache (url, etag, content, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(url) DO UPDATE SET - etag = excluded.etag, - content = excluded.content, - updated_at = excluded.updated_at - `; - return { - dbPath: resolvedPath, - /** The last-known `{ etag, content }` for a policy-doc URL, or null when it has never been cached. Both columns - * are `TEXT NOT NULL`, so a present row always carries string values. */ - get(url) { - const { rows } = driver.query(getSql, [normalizeUrl(url)]); - const row = rows[0]; - return row ? { etag: row.etag, content: row.content } : null; - }, - /** Record the fresh ETag + body so the next run can revalidate it with a conditional GET. */ - put(url, etag, content) { - const normalizedUrl = normalizeUrl(url); - if (typeof etag !== "string" || !etag.trim()) - throw new Error("invalid_policy_doc_etag"); - if (typeof content !== "string") - throw new Error("invalid_policy_doc_content"); - const updatedAt = new Date().toISOString(); - driver.query(putSql, [normalizedUrl, etag, content, updatedAt]); - return { url: normalizedUrl, etag, content, updatedAt }; - }, - close() { - db.close(); - }, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9saWN5LWRvYy1jYWNoZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBvbGljeS1kb2MtY2FjaGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHlCQUF5QixFQUFFLHFCQUFxQixFQUFFLHVCQUF1QixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDN0csT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFNUQsNkdBQTZHO0FBQzdHLGdIQUFnSDtBQUNoSCxnSEFBZ0g7QUFDaEgsa0hBQWtIO0FBQ2xILG1IQUFtSDtBQUNuSCw2R0FBNkc7QUFDN0csaUhBQWlIO0FBQ2pILDBGQUEwRjtBQUUxRixNQUFNLGlCQUFpQixHQUFHLDBCQUEwQixDQUFDO0FBd0JyRCxNQUFNLFVBQVUsMkJBQTJCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDL0YsT0FBTyx1QkFBdUIsQ0FBQyxpQkFBaUIsRUFBRSxvQ0FBb0MsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUMvRixDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsTUFBYztJQUNyQyxPQUFPLHlCQUF5QixDQUFDLE1BQU0sRUFBRSwyQkFBMkIsRUFBRSxFQUFFLGtDQUFrQyxDQUFDLENBQUM7QUFDOUcsQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEdBQVk7SUFDaEMsSUFBSSxPQUFPLEdBQUcsS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ3ZFLE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUMzQixJQUFJLENBQUMsT0FBTztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUN4RCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxTQUFpQiwyQkFBMkIsRUFBRTtJQUNwRixNQUFNLFlBQVksR0FBRyxlQUFlLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDN0MsTUFBTSxFQUFFLEVBQUUsRUFBRSxNQUFNLEVBQUUsR0FBRyxxQkFBcUIsQ0FBQyxZQUFZLENBQUMsQ0FBQztJQUMzRCxFQUFFLENBQUMsSUFBSSxDQUFDOzs7Ozs7O0dBT1AsQ0FBQyxDQUFDO0lBQ0gseUdBQXlHO0lBQ3pHLHFCQUFxQixDQUFDLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUU5QixNQUFNLE1BQU0sR0FBRywwREFBMEQsQ0FBQztJQUMxRSxNQUFNLE1BQU0sR0FBRzs7Ozs7OztHQU9kLENBQUM7SUFFRixPQUFPO1FBQ0wsTUFBTSxFQUFFLFlBQVk7UUFDcEI7a0ZBQzBFO1FBQzFFLEdBQUcsQ0FBQyxHQUFHO1lBQ0wsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUMzRCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFrRCxDQUFDO1lBQ3JFLE9BQU8sR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLElBQUksRUFBRSxHQUFHLENBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxHQUFHLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUMvRCxDQUFDO1FBQ0QsNkZBQTZGO1FBQzdGLEdBQUcsQ0FBQyxHQUFHLEVBQUUsSUFBSSxFQUFFLE9BQU87WUFDcEIsTUFBTSxhQUFhLEdBQUcsWUFBWSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3hDLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRTtnQkFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHlCQUF5QixDQUFDLENBQUM7WUFDekYsSUFBSSxPQUFPLE9BQU8sS0FBSyxRQUFRO2dCQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsNEJBQTRCLENBQUMsQ0FBQztZQUMvRSxNQUFNLFNBQVMsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO1lBQzNDLE1BQU0sQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsYUFBYSxFQUFFLElBQUksRUFBRSxPQUFPLEVBQUUsU0FBUyxDQUFDLENBQUMsQ0FBQztZQUNoRSxPQUFPLEVBQUUsR0FBRyxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxDQUFDO1FBQzFELENBQUM7UUFDRCxLQUFLO1lBQ0gsRUFBRSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ2IsQ0FBQztLQUNGLENBQUM7QUFDSixDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/policy-verdict-cache.d.ts b/packages/loopover-miner/lib/policy-verdict-cache.d.ts deleted file mode 100644 index ff92ea76f5..0000000000 --- a/packages/loopover-miner/lib/policy-verdict-cache.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AiPolicyVerdict } from "@loopover/engine"; -export type PolicyVerdictDecisiveDoc = "AI-USAGE.md" | "CONTRIBUTING.md"; -export type PolicyVerdictCacheEntry = { - decisiveDoc: PolicyVerdictDecisiveDoc; - etag: string; - verdict: AiPolicyVerdict; -}; -export type PolicyVerdictCacheWrite = PolicyVerdictCacheEntry & { - repoScope: string; - updatedAt: string; -}; -export type PolicyVerdictCacheStore = { - dbPath: string; - /** `repoScope` must uniquely identify a tenant forge host + repo (see `policyVerdictCacheKey` in - * opportunity-fanout.js) -- a bare `owner/repo` is not safe across multiple forge hosts. */ - get(repoScope: string): PolicyVerdictCacheEntry | null; - put(repoScope: string, decisiveDoc: PolicyVerdictDecisiveDoc, etag: string, verdict: AiPolicyVerdict): PolicyVerdictCacheWrite; - /** Delete every cached verdict row for one repo scope (#6987); returns the number of rows removed. */ - purgeByRepo(repoScope: string): number; - close(): void; -}; -/** The read/write surface opportunity-fanout.js needs to inject a cache without depending on the SQLite store. */ -export type PolicyVerdictCache = Pick; -export declare function resolvePolicyVerdictCacheDbPath(env?: Record): string; -/** - * Opens the 100% local/client-side miner policy-verdict cache. The database only lives on this machine; this - * module never uploads, syncs, or phones home with its contents. (#4843) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations and the repo-scoped purge still use the underlying DatabaseSync until those - * helpers are migrated. Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export declare function initPolicyVerdictCacheStore(dbPath?: string): PolicyVerdictCacheStore; diff --git a/packages/loopover-miner/lib/policy-verdict-cache.js b/packages/loopover-miner/lib/policy-verdict-cache.js deleted file mode 100644 index d08649b403..0000000000 --- a/packages/loopover-miner/lib/policy-verdict-cache.js +++ /dev/null @@ -1,104 +0,0 @@ -import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { POLICY_VERDICT_CACHE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; -const defaultDbFileName = "policy-verdict-cache.sqlite3"; -const DECISIVE_DOCS = new Set(["AI-USAGE.md", "CONTRIBUTING.md"]); -export function resolvePolicyVerdictCacheDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolvePolicyVerdictCacheDbPath(), "invalid_policy_verdict_cache_db_path"); -} -function normalizeRepoScope(repoScope) { - if (typeof repoScope !== "string") - throw new Error("invalid_policy_verdict_repo_scope"); - const trimmed = repoScope.trim(); - if (!trimmed) - throw new Error("invalid_policy_verdict_repo_scope"); - return trimmed; -} -function normalizeDecisiveDoc(decisiveDoc) { - if (!DECISIVE_DOCS.has(decisiveDoc)) - throw new Error("invalid_policy_verdict_decisive_doc"); - return decisiveDoc; -} -function normalizeEtag(etag) { - if (typeof etag !== "string" || !etag.trim()) - throw new Error("invalid_policy_verdict_etag"); - return etag; -} -function serializeVerdict(verdict) { - if (!verdict || typeof verdict !== "object" || Array.isArray(verdict)) { - throw new Error("invalid_policy_verdict"); - } - return JSON.stringify(verdict); -} -/** - * Opens the 100% local/client-side miner policy-verdict cache. The database only lives on this machine; this - * module never uploads, syncs, or phones home with its contents. (#4843) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema creation/migrations and the repo-scoped purge still use the underlying DatabaseSync until those - * helpers are migrated. Public API stays synchronous so callers need no async cascade in this part-1 slice. - */ -export function initPolicyVerdictCacheStore(dbPath = resolvePolicyVerdictCacheDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS policy_verdict_cache ( - repo_scope TEXT PRIMARY KEY, - decisive_doc TEXT NOT NULL, - etag TEXT NOT NULL, - verdict TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations (none yet). - applySchemaMigrations(db, []); - const getSql = "SELECT decisive_doc, etag, verdict FROM policy_verdict_cache WHERE repo_scope = ?"; - const putSql = ` - INSERT INTO policy_verdict_cache (repo_scope, decisive_doc, etag, verdict, updated_at) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(repo_scope) DO UPDATE SET - decisive_doc = excluded.decisive_doc, - etag = excluded.etag, - verdict = excluded.verdict, - updated_at = excluded.updated_at - `; - return { - dbPath: resolvedPath, - /** The last-known `{ decisiveDoc, etag, verdict }` for a repo scope, or null when it has never been cached. */ - get(repoScope) { - const row = driver.query(getSql, [normalizeRepoScope(repoScope)]).rows[0]; - if (!row) - return null; - return { - decisiveDoc: row.decisive_doc, - etag: row.etag, - verdict: JSON.parse(row.verdict), - }; - }, - /** Record the resolved verdict against the ETag of the doc that decided it, so the next run can reuse it. */ - put(repoScope, decisiveDoc, etag, verdict) { - const normalizedRepoScope = normalizeRepoScope(repoScope); - const normalizedDecisiveDoc = normalizeDecisiveDoc(decisiveDoc); - const normalizedEtag = normalizeEtag(etag); - const serializedVerdict = serializeVerdict(verdict); - const updatedAt = new Date().toISOString(); - driver.query(putSql, [normalizedRepoScope, normalizedDecisiveDoc, normalizedEtag, serializedVerdict, updatedAt]); - return { repoScope: normalizedRepoScope, decisiveDoc: normalizedDecisiveDoc, etag: normalizedEtag, verdict, updatedAt }; - }, - /** - * Delete every cached verdict row for one repo scope (#6987) -- the right-to-be-forgotten path - * `loopover-miner purge` invokes. Returns the number of rows removed. Reuses store-maintenance.js's - * identifier-guarded purgeStoreByRepo, exactly like the other repo-scoped stores. - */ - purgeByRepo(repoScope) { - return purgeStoreByRepo(db, POLICY_VERDICT_CACHE_PURGE_SPEC, normalizeRepoScope(repoScope)); - }, - close() { - db.close(); - }, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9saWN5LXZlcmRpY3QtY2FjaGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwb2xpY3ktdmVyZGljdC1jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUseUJBQXlCLEVBQUUscUJBQXFCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM3RyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUM1RCxPQUFPLEVBQUUsK0JBQStCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQXFEM0YsTUFBTSxpQkFBaUIsR0FBRyw4QkFBOEIsQ0FBQztBQUN6RCxNQUFNLGFBQWEsR0FBRyxJQUFJLEdBQUcsQ0FBUyxDQUFDLGFBQWEsRUFBRSxpQkFBaUIsQ0FBQyxDQUFDLENBQUM7QUFFMUUsTUFBTSxVQUFVLCtCQUErQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQ25HLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsd0NBQXdDLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDbkcsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWlDO0lBQ3hELE9BQU8seUJBQXlCLENBQUMsTUFBTSxFQUFFLCtCQUErQixFQUFFLEVBQUUsc0NBQXNDLENBQUMsQ0FBQztBQUN0SCxDQUFDO0FBRUQsU0FBUyxrQkFBa0IsQ0FBQyxTQUFrQjtJQUM1QyxJQUFJLE9BQU8sU0FBUyxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLG1DQUFtQyxDQUFDLENBQUM7SUFDeEYsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ2pDLElBQUksQ0FBQyxPQUFPO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxtQ0FBbUMsQ0FBQyxDQUFDO0lBQ25FLE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUM7QUFFRCxTQUFTLG9CQUFvQixDQUFDLFdBQW9CO0lB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-dashboard.d.ts b/packages/loopover-miner/lib/portfolio-dashboard.d.ts deleted file mode 100644 index c6274ccef7..0000000000 --- a/packages/loopover-miner/lib/portfolio-dashboard.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface PortfolioRepoSummary { - apiBaseUrl: string; - repoFullName: string; - byStatus: { - queued: number; - in_progress: number; - done: number; - }; - total: number; -} -export interface PortfolioDashboardSummary { - total: number; - byStatus: { - queued: number; - in_progress: number; - done: number; - }; - repos: PortfolioRepoSummary[]; - oldestQueuedAgeMs: number | null; -} -export interface PortfolioDashboardSources { - portfolioQueue: { - listQueue(repoFullName?: string | null): unknown[]; - }; -} -/** - * Pure aggregator over an injected portfolio-queue store (mirrors manage-status.js's `collectManageStatus`). - * Read-only. Returns global + per-repo status counts and, when a clock is supplied via `options.nowMs`, the age in - * ms of the oldest still-`queued` item (null when no clock is given or nothing is queued). - */ -export declare function collectPortfolioDashboard(sources: PortfolioDashboardSources, options?: { - nowMs?: number; -}): PortfolioDashboardSummary; -/** Plain-text render of a dashboard summary (mirrors manage-status.js's `renderManageStatusTable`). */ -export declare function renderPortfolioDashboardTable(summary: PortfolioDashboardSummary | null | undefined): string; -export declare function parsePortfolioDashboardArgs(args?: string[]): { - json: boolean; -} | { - error: string; -}; -/** CLI glue for `loopover-miner queue dashboard [--json]` (mirrors manage-status.js's `runManageStatus`). */ -export declare function runPortfolioDashboard(args?: string[], options?: { - initPortfolioQueue?: () => { - listQueue(repoFullName: string | null): unknown[]; - close(): void; - }; - nowMs?: number; -}): number; diff --git a/packages/loopover-miner/lib/portfolio-dashboard.js b/packages/loopover-miner/lib/portfolio-dashboard.js deleted file mode 100644 index 0640270d57..0000000000 --- a/packages/loopover-miner/lib/portfolio-dashboard.js +++ /dev/null @@ -1,119 +0,0 @@ -// Read-only portfolio-queue dashboard (#4287). Aggregates the miner's OWN local portfolio-queue backlog -// (packages/loopover-miner/lib/portfolio-queue.js) into summary stats — counts by status globally and per repo, -// plus the oldest queued item's age. Same three-layer shape as manage-status.js (pure collect → pure render → thin -// CLI glue), but scoped to the backlog/queue rather than per-PR manage state. 100% client-side, read-only — it never -// mutates queue state and never gates or enforces anything. -// -// The extension-panel half named in the issue is a forward dependency, not delivered here: the miner's queue is a -// local SQLite file with no local-reachable channel a GitHub-page content script can read today. The pure -// collector below is factored so it is directly reusable once such a channel exists. -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -const QUEUE_STATUS_KEYS = ["queued", "in_progress", "done"]; -function emptyCounts() { - return { queued: 0, in_progress: 0, done: 0 }; -} -function isQueueStatusKey(status) { - return typeof status === "string" && QUEUE_STATUS_KEYS.includes(status); -} -/** - * Pure aggregator over an injected portfolio-queue store (mirrors manage-status.js's `collectManageStatus`). - * Read-only. Returns global + per-repo status counts and, when a clock is supplied via `options.nowMs`, the age in - * ms of the oldest still-`queued` item (null when no clock is given or nothing is queued). - */ -export function collectPortfolioDashboard(sources, options = {}) { - const portfolioQueue = sources?.portfolioQueue; - if (!portfolioQueue || typeof portfolioQueue.listQueue !== "function") - throw new Error("invalid_portfolio_queue"); - const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : null; - const byStatus = emptyCounts(); - const perRepo = new Map(); - let total = 0; - let oldestQueuedMs = null; - for (const raw of portfolioQueue.listQueue(null)) { - const entry = raw; - const status = entry?.status; - if (!isQueueStatusKey(status)) - continue; - const repoFullName = typeof entry.repoFullName === "string" ? entry.repoFullName : ""; - // #7225: key per-repo backlogs by (apiBaseUrl, repoFullName) so two forge hosts sharing a repo name keep - // independent counts instead of silently merging. The composite map key uses "\n" — never valid in either - // component — so distinct (host, repo) pairs can never collide. - const apiBaseUrl = typeof entry.apiBaseUrl === "string" ? entry.apiBaseUrl : ""; - total += 1; - byStatus[status] += 1; - const key = `${apiBaseUrl}\n${repoFullName}`; - let repo = perRepo.get(key); - if (!repo) { - repo = { apiBaseUrl, repoFullName, byStatus: emptyCounts(), total: 0 }; - perRepo.set(key, repo); - } - repo.byStatus[status] += 1; - repo.total += 1; - if (status === "queued") { - const ms = Date.parse(String(entry.enqueuedAt)); - if (Number.isFinite(ms) && (oldestQueuedMs === null || ms < oldestQueuedMs)) - oldestQueuedMs = ms; - } - } - const repos = [...perRepo.values()].sort((left, right) => left.repoFullName.localeCompare(right.repoFullName) || left.apiBaseUrl.localeCompare(right.apiBaseUrl)); - const oldestQueuedAgeMs = nowMs !== null && oldestQueuedMs !== null ? Math.max(0, nowMs - oldestQueuedMs) : null; - return { total, byStatus, repos, oldestQueuedAgeMs }; -} -/** Plain-text render of a dashboard summary (mirrors manage-status.js's `renderManageStatusTable`). */ -export function renderPortfolioDashboardTable(summary) { - if (!summary || summary.total === 0) - return "portfolio queue is empty"; - const age = summary.oldestQueuedAgeMs !== null ? ` oldest-queued: ${Math.round(summary.oldestQueuedAgeMs / 60000)}m` : ""; - const header = [ - "repo".padEnd(28), - "host".padEnd(30), - "queued".padStart(7), - "in_prog".padStart(8), - "done".padStart(6), - "total".padStart(6), - ].join(" "); - const lines = summary.repos.map((repo) => [ - repo.repoFullName.padEnd(28), - String(repo.apiBaseUrl).padEnd(30), - String(repo.byStatus.queued).padStart(7), - String(repo.byStatus.in_progress).padStart(8), - String(repo.byStatus.done).padStart(6), - String(repo.total).padStart(6), - ].join(" ")); - return [ - `total: ${summary.total} queued: ${summary.byStatus.queued} in_progress: ${summary.byStatus.in_progress} done: ${summary.byStatus.done}${age}`, - "", - header, - ...lines, - ].join("\n"); -} -export function parsePortfolioDashboardArgs(args = []) { - for (const token of args) { - if (token === "--json") - continue; - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - return { error: "Usage: loopover-miner queue dashboard [--json]" }; - } - return { json: args.includes("--json") }; -} -/** CLI glue for `loopover-miner queue dashboard [--json]` (mirrors manage-status.js's `runManageStatus`). */ -export function runPortfolioDashboard(args = [], options = {}) { - const parsed = parsePortfolioDashboardArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const ownsQueue = options.initPortfolioQueue === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - try { - const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: Number.isFinite(options.nowMs) ? options.nowMs : Date.now() }); - console.log(parsed.json ? JSON.stringify(summary, null, 2) : renderPortfolioDashboardTable(summary)); - return 0; - } - finally { - if (ownsQueue) - portfolioQueue.close(); - } -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLWRhc2hib2FyZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBvcnRmb2xpby1kYXNoYm9hcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsd0dBQXdHO0FBQ3hHLGdIQUFnSDtBQUNoSCxtSEFBbUg7QUFDbkgscUhBQXFIO0FBQ3JILDREQUE0RDtBQUM1RCxFQUFFO0FBQ0Ysa0hBQWtIO0FBQ2xILDBHQUEwRztBQUMxRyxxRkFBcUY7QUFFckYsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDL0QsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWhFLE1BQU0saUJBQWlCLEdBQUcsQ0FBQyxRQUFRLEVBQUUsYUFBYSxFQUFFLE1BQU0sQ0FBVSxDQUFDO0FBNEJyRSxTQUFTLFdBQVc7SUFDbEIsT0FBTyxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUUsV0FBVyxFQUFFLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUM7QUFDaEQsQ0FBQztBQUVELFNBQVMsZ0JBQWdCLENBQUMsTUFBZTtJQUN2QyxPQUFPLE9BQU8sTUFBTSxLQUFLLFFBQVEsSUFBSyxpQkFBdUMsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDakcsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUseUJBQXlCLENBQ3ZDLE9BQWtDLEVBQ2xDLFVBQThCLEVBQUU7SUFFaEMsTUFBTSxjQUFjLEdBQUcsT0FBTyxFQUFFLGNBQWMsQ0FBQztJQUMvQyxJQUFJLENBQUMsY0FBYyxJQUFJLE9BQU8sY0FBYyxDQUFDLFNBQVMsS0FBSyxVQUFVO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQ2xILE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBRSxPQUFP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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-discovery.d.ts b/packages/loopover-miner/lib/portfolio-discovery.d.ts deleted file mode 100644 index c7c5560030..0000000000 --- a/packages/loopover-miner/lib/portfolio-discovery.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** Local orchestration: materialize ranked fan-out rows into the portfolio queue (#2292). */ -import type { EventLedger } from "./event-ledger.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -export type EnqueueRankedDiscoveryInput = { - repoFullName: string; - issueNumber: number; - title: string; - labels?: string[]; - rankScore: number; -}; -export type EnqueueRankedDiscoveryOptions = { - queueStore: PortfolioQueueStore; - eventLedger?: EventLedger; - minRankScore?: number | null; - apiBaseUrl?: string; -}; -export type EnqueueRankedDiscoverySummary = { - enqueued: number; - skippedBelowMinRank: number; - skippedInvalid: number; - eventsAppended: number; -}; -/** - * Enqueue ranked discovery rows into the local portfolio backlog. Uses each row's `rankScore` as queue priority - * (the #2292 placeholder field). Optionally appends `discovered_issue` audit events when an event ledger is supplied. - * Never calls GitHub — callers rank locally first via `rankCandidateIssues`. - */ -export declare function enqueueRankedDiscovery(rankedIssues: readonly EnqueueRankedDiscoveryInput[], options: EnqueueRankedDiscoveryOptions): EnqueueRankedDiscoverySummary; diff --git a/packages/loopover-miner/lib/portfolio-discovery.js b/packages/loopover-miner/lib/portfolio-discovery.js deleted file mode 100644 index c27ee18020..0000000000 --- a/packages/loopover-miner/lib/portfolio-discovery.js +++ /dev/null @@ -1,100 +0,0 @@ -function normalizeMinRankScore(minRankScore) { - if (minRankScore === undefined || minRankScore === null) - return 0; - if (typeof minRankScore !== "number" || !Number.isFinite(minRankScore) || minRankScore < 0) { - throw new Error("invalid_min_rank_score"); - } - return minRankScore; -} -function normalizeRankedIssue(issue) { - if (!issue || typeof issue !== "object") - return null; - const i = issue; - const repoFullName = typeof i.repoFullName === "string" ? i.repoFullName.trim() : ""; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - if (!Number.isInteger(i.issueNumber) || i.issueNumber <= 0) - return null; - if (typeof i.rankScore !== "number" || !Number.isFinite(i.rankScore) || i.rankScore < 0) { - return null; - } - const title = typeof i.title === "string" ? i.title.trim() : ""; - if (!title) - return null; - const labels = Array.isArray(i.labels) - ? i.labels.filter((label) => typeof label === "string" && label.trim() !== "").map((label) => label.trim()) - : []; - return { - repoFullName: `${owner}/${repo}`, - issueNumber: i.issueNumber, - title, - labels, - rankScore: i.rankScore, - }; -} -/** - * Enqueue ranked discovery rows into the local portfolio backlog. Uses each row's `rankScore` as queue priority - * (the #2292 placeholder field). Optionally appends `discovered_issue` audit events when an event ledger is supplied. - * Never calls GitHub — callers rank locally first via `rankCandidateIssues`. - */ -export function enqueueRankedDiscovery(rankedIssues, options) { - if (!Array.isArray(rankedIssues)) - throw new Error("invalid_ranked_issues"); - const queueStore = options.queueStore; - if (!queueStore || typeof queueStore.enqueue !== "function") - throw new Error("invalid_queue_store"); - let eventLedger = null; - if (options.eventLedger !== undefined) { - eventLedger = options.eventLedger; - if (!eventLedger || typeof eventLedger.appendEvent !== "function") { - throw new Error("invalid_event_ledger"); - } - } - const minRankScore = normalizeMinRankScore(options.minRankScore); - // #5563: threaded through from the caller's already-resolved forge host, so a non-default (GitHub Enterprise) - // tenant's ranked issues land in the queue scoped to their own host instead of colliding with a same-named - // owner/repo on github.com. Omitted/nullish falls through to the queue store's own github.com default. - const apiBaseUrl = options.apiBaseUrl; - const summary = { - enqueued: 0, - skippedBelowMinRank: 0, - skippedInvalid: 0, - eventsAppended: 0, - }; - for (const issue of rankedIssues) { - const normalized = normalizeRankedIssue(issue); - if (!normalized) { - summary.skippedInvalid += 1; - continue; - } - if (normalized.rankScore < minRankScore) { - summary.skippedBelowMinRank += 1; - continue; - } - // Spread-omit rather than pass `undefined` explicitly -- EnqueueItem's `apiBaseUrl` doesn't declare - // `| undefined`, and exactOptionalPropertyTypes treats those as different. - queueStore.enqueue({ - repoFullName: normalized.repoFullName, - identifier: `issue:${normalized.issueNumber}`, - priority: normalized.rankScore, - ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}), - }); - summary.enqueued += 1; - if (eventLedger) { - eventLedger.appendEvent({ - type: "discovered_issue", - repoFullName: normalized.repoFullName, - payload: { - issueNumber: normalized.issueNumber, - rankScore: normalized.rankScore, - title: normalized.title, - labels: normalized.labels, - }, - }); - summary.eventsAppended += 1; - } - } - return summary; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLWRpc2NvdmVyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBvcnRmb2xpby1kaXNjb3ZlcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBa0NBLFNBQVMscUJBQXFCLENBQUMsWUFBdUM7SUFDcEUsSUFBSSxZQUFZLEtBQUssU0FBUyxJQUFJLFlBQVksS0FBSyxJQUFJO1FBQUUsT0FBTyxDQUFDLENBQUM7SUFDbEUsSUFBSSxPQUFPLFlBQVksS0FBSyxRQUFRLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxJQUFJLFlBQVksR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUMzRixNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUNELE9BQU8sWUFBWSxDQUFDO0FBQ3RCLENBQUM7QUFFRCxTQUFTLG9CQUFvQixDQUFDLEtBQWM7SUFDMUMsSUFBSSxDQUFDLEtBQUssSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDckQsTUFBTSxDQUFDLEdBQUcsS0FBZ0MsQ0FBQztJQUMzQyxNQUFNLFlBQVksR0FBRyxPQUFPLENBQUMsQ0FBQyxZQUFZLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDckYsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNyRCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDeEQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxJQUFLLENBQUMsQ0FBQyxXQUFzQixJQUFJLENBQUM7UUFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-queue-cli.d.ts b/packages/loopover-miner/lib/portfolio-queue-cli.d.ts deleted file mode 100644 index dc7865070b..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-cli.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { PortfolioQueueStore, QueueEntry } from "./portfolio-queue.js"; -import type { PortfolioQueueManager } from "./portfolio-queue-manager.js"; -export type ParsedQueueListArgs = { - json: boolean; - repoFullName: string | null; -} | { - error: string; -}; -export type ParsedQueueNextArgs = { - json: boolean; - dryRun: boolean; - globalWipCap: number | undefined; - perRepoWipCap: number | undefined; -} | { - error: string; -}; -export type QueueClaimTarget = { - repoFullName: string; - identifier: string; - apiBaseUrl: string; -}; -export type ParsedQueueDoneArgs = { - repoFullName: string; - identifier: string; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; -} | { - error: string; -}; -export type ParsedQueueClaimBatchArgs = { - json: boolean; - dryRun: boolean; - globalWipCap: number; - perRepoWipCap: number; -} | { - error: string; -}; -export declare function parseQueueListArgs(args: string[]): ParsedQueueListArgs; -export declare function parseQueueNextArgs(args: string[]): ParsedQueueNextArgs; -/** - * Pick at most one atomically-claimable target from the store's already-priority-ordered active rows (queued - * AND in_progress interleaved, exactly `batchClaim`'s own `entries` shape). `caps` of `null` replicates the - * pre-#4850 behavior: the single highest-priority queued row, unconditionally. When caps are set, refuses to - * select anything once the global or the target row's own per-repo in-progress count has reached its cap -- - * "stops claiming once the cap is reached" (#4850), not a diversifying batch selection (that remains - * claim-batch's job via the engine's own `nextEligibleItems`). - * @param {Array<{ repoFullName: string, identifier: string, apiBaseUrl: string, status: string }>} entries - * @param {{ globalWipCap: number, perRepoWipCap: number } | null} caps - */ -export declare function selectNextEligibleTarget(entries: Array<{ - repoFullName: string; - identifier: string; - apiBaseUrl: string; - status: string; -}>, caps: { - globalWipCap: number; - perRepoWipCap: number; -} | null): QueueClaimTarget[]; -export declare function parseQueueDoneArgs(args: string[]): ParsedQueueDoneArgs; -export declare function parseQueueReleaseArgs(args: string[]): ParsedQueueDoneArgs; -export declare function parseQueueRequeueArgs(args: string[]): ParsedQueueDoneArgs; -export declare function renderQueueTable(entries: QueueEntry[]): string; -export declare function runQueueList(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; -}): number; -export declare function runQueueNext(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; -}): number; -export declare function runQueueDone(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; -}): number; -/** `release `: manually give up a CLAIMED (in_progress) item, returning it to the queue - * (the manual counterpart to the automated stuck-lease sweep). Exit 2 when there is no in-flight item to release. */ -export declare function runQueueRelease(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; -}): number; -/** `requeue `: manually put a COMPLETED (done) item back on the queue so it is picked up - * again, keeping its original FIFO position. Exit 2 when there is no done item to requeue (already queued, - * in-flight — release it instead — or absent). */ -export declare function runQueueRequeue(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; -}): number; -export declare function parseQueueClaimBatchArgs(args: string[]): ParsedQueueClaimBatchArgs; -/** Claim the next caps-aware batch via the WIP-cap-aware batch claimer (portfolio-queue-manager.js), which also - * reclaims any leases orphaned by a crashed process first (#4833 wires the previously caller-less claimer). */ -export declare function runQueueClaimBatch(args: string[], options?: { - initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager; -}): number; -export declare const QUEUE_ITEMS = "loopover_miner_portfolio_queue_items"; -export declare const QUEUE_OLDEST_IN_PROGRESS_LEASE_AGE_SECONDS = "loopover_miner_portfolio_queue_oldest_in_progress_lease_age_seconds"; -/** - * Render portfolio-queue backlog health as Prometheus text-exposition gauges: current item count per status, and - * the age of the OLDEST still-in-flight lease -- the concrete "is anything stuck" signal a - * `loopover_queue_oldest_maintenance_pending_age_seconds`-style alert rule can threshold on (#5186). Pure and - * side-effect-free: the caller supplies the rows and `nowMs` (no internal clock read, matching - * store-maintenance.js's pruneLedgerByRetention convention) and prints the result. Deterministic (status series - * sorted); always emits HELP/TYPE so an empty queue is still a well-formed exposition document, and the lease-age - * gauge reads 0 (never stuck) rather than being omitted when nothing is in-flight. - * @param {Array<{ status: string }>} queueEntries - every row, any status (e.g. store.listQueue()'s output). - * @param {Array<{ leasedAt: string | null }>} leaseEntries - in-flight rows only (store.listInProgress()'s output). - * @param {number} nowMs - */ -export declare function renderPortfolioQueueMetrics(queueEntries: Array<{ - status: string; -}>, leaseEntries: Array<{ - leasedAt: string | null; -}>, nowMs: number): string; -export declare function runQueueMetrics(args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; - nowMs?: number; -}): number; -export declare function runQueueCli(subcommand: string | undefined, args: string[], options?: { - initPortfolioQueue?: () => PortfolioQueueStore; - initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager; -}): number; diff --git a/packages/loopover-miner/lib/portfolio-queue-cli.js b/packages/loopover-miner/lib/portfolio-queue-cli.js deleted file mode 100644 index b44c15b169..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-cli.js +++ /dev/null @@ -1,566 +0,0 @@ -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { initPortfolioQueueManager } from "./portfolio-queue-manager.js"; -import { runPortfolioDashboard } from "./portfolio-dashboard.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -const QUEUE_LIST_USAGE = "Usage: loopover-miner queue list [--repo ] [--json]"; -const QUEUE_NEXT_USAGE = "Usage: loopover-miner queue next [--global-wip ] [--per-repo-wip ] [--dry-run] [--json]"; -const QUEUE_DONE_USAGE = "Usage: loopover-miner queue done [--api-base-url ] [--dry-run] [--json]"; -const QUEUE_RELEASE_USAGE = "Usage: loopover-miner queue release [--api-base-url ] [--dry-run] [--json]"; -const QUEUE_REQUEUE_USAGE = "Usage: loopover-miner queue requeue [--api-base-url ] [--dry-run] [--json]"; -const QUEUE_CLAIM_BATCH_USAGE = "Usage: loopover-miner queue claim-batch [--global-wip ] [--per-repo-wip ] [--dry-run] [--json]"; -function parseRepoArg(value, usage) { - if (!value) - return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - // #7525: extend #5831's path-safety guard to this CLI filter too — a `.`/`..`/control-char segment must not - // reach the queue query. Reuse the same error shape as the malformed-input branch above (don't invent one). - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -export function parseQueueListArgs(args) { - const options = { json: false, repoFullName: null }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) { - return { error: QUEUE_LIST_USAGE }; - } - const repo = parseRepoArg(repoArg, QUEUE_LIST_USAGE); - if ("error" in repo) - return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length > 0) { - return { error: QUEUE_LIST_USAGE }; - } - return options; -} -// #4850: --global-wip/--per-repo-wip are OMITTED (undefined) by default -- queue next stays uncapped, byte- -// identical to its pre-#4850 behavior, unless an operator explicitly opts in. Mirrors queue claim-batch's own -// flag names (portfolio-queue-manager.js's WIP-cap-aware claimer), but claim-batch's OWN default of 1/1 is not -// reused here: claim-batch's whole purpose is cap enforcement, while queue next has always been a plain -// highest-priority dequeue and must not silently start capping existing callers that never asked for it. -export function parseQueueNextArgs(args) { - const options = { json: false, dryRun: false, globalWipCap: undefined, perRepoWipCap: undefined }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--global-wip" || token === "--per-repo-wip") { - const value = Number(args[index + 1]); - if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) { - return { error: QUEUE_NEXT_USAGE }; - } - if (token === "--global-wip") - options.globalWipCap = value; - else - options.perRepoWipCap = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length > 0) { - return { error: QUEUE_NEXT_USAGE }; - } - return options; -} -/** - * Pick at most one atomically-claimable target from the store's already-priority-ordered active rows (queued - * AND in_progress interleaved, exactly `batchClaim`'s own `entries` shape). `caps` of `null` replicates the - * pre-#4850 behavior: the single highest-priority queued row, unconditionally. When caps are set, refuses to - * select anything once the global or the target row's own per-repo in-progress count has reached its cap -- - * "stops claiming once the cap is reached" (#4850), not a diversifying batch selection (that remains - * claim-batch's job via the engine's own `nextEligibleItems`). - * @param {Array<{ repoFullName: string, identifier: string, apiBaseUrl: string, status: string }>} entries - * @param {{ globalWipCap: number, perRepoWipCap: number } | null} caps - */ -export function selectNextEligibleTarget(entries, caps) { - const topQueued = entries.find((entry) => entry.status === "queued"); - if (!topQueued) - return []; - if (!caps) { - return [{ repoFullName: topQueued.repoFullName, identifier: topQueued.identifier, apiBaseUrl: topQueued.apiBaseUrl }]; - } - const globalActiveCount = entries.filter((entry) => entry.status === "in_progress").length; - if (globalActiveCount >= caps.globalWipCap) - return []; - // Host-scope the per-repo active count (#7224): a same-named repo on a DIFFERENT forge host is a distinct backlog - // (the store keys rows by apiBaseUrl too, #5563), so an in-progress item on host A must not consume host B's - // per-repo WIP budget. Single-host is unchanged: every entry shares one apiBaseUrl, so the added match is always true. - const repoActiveCount = entries.filter((entry) => entry.status === "in_progress" && - entry.repoFullName === topQueued.repoFullName && - entry.apiBaseUrl === topQueued.apiBaseUrl).length; - if (repoActiveCount >= caps.perRepoWipCap) - return []; - return [{ repoFullName: topQueued.repoFullName, identifier: topQueued.identifier, apiBaseUrl: topQueued.apiBaseUrl }]; -} -/** Shared ` [--api-base-url ] [--json]` parse for the item-targeting subcommands - * (done/release/requeue). `usage` is the command-specific message surfaced on a malformed argv. */ -function parseRepoIdentifierArgs(args, usage) { - const options = { - json: false, - dryRun: false, - apiBaseUrl: undefined, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what a real mutation would do and returns before opening the portfolio queue at all. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - // #5563: scope the target to a non-default forge host, so it doesn't collide with (or get confused for) a - // same-named repo on the default github.com host. - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: usage }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 2) { - return { error: usage }; - } - const repo = parseRepoArg(positional[0], usage); - if ("error" in repo) - return repo; - const identifier = positional[1]?.trim(); - if (!identifier) { - return { error: usage }; - } - return { - repoFullName: repo.repoFullName, - identifier, - dryRun: options.dryRun, - json: options.json, - apiBaseUrl: options.apiBaseUrl, - }; -} -export function parseQueueDoneArgs(args) { - return parseRepoIdentifierArgs(args, QUEUE_DONE_USAGE); -} -export function parseQueueReleaseArgs(args) { - return parseRepoIdentifierArgs(args, QUEUE_RELEASE_USAGE); -} -export function parseQueueRequeueArgs(args) { - return parseRepoIdentifierArgs(args, QUEUE_REQUEUE_USAGE); -} -function display(value) { - if (value === null || value === undefined) - return "-"; - return String(value); -} -export function renderQueueTable(entries) { - if (!Array.isArray(entries) || entries.length === 0) - return "no portfolio queue entries"; - const header = [ - "repo".padEnd(24), - "identifier".padEnd(16), - // #7225: surface the host so a reader of the plain-text table can supply the `--api-base-url` a follow-up - // done/release/requeue needs to disambiguate two rows sharing a repo+identifier across forge hosts. - "host".padEnd(30), - "status".padEnd(12), - "pri".padStart(4), - "enqueued-at".padEnd(24), - ].join(" "); - const lines = entries.map((entry) => [ - entry.repoFullName.padEnd(24), - entry.identifier.padEnd(16), - display(entry.apiBaseUrl).padEnd(30), - entry.status.padEnd(12), - display(entry.priority).padStart(4), - display(entry.enqueuedAt).padEnd(24), - ].join(" ")); - return [header, ...lines].join("\n"); -} -function withPortfolioQueue(options, run) { - const ownsStore = options.initPortfolioQueue === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - try { - return run(portfolioQueue); - } - finally { - if (ownsStore) - portfolioQueue.close(); - } -} -export function runQueueList(args, options = {}) { - const parsed = parseQueueListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - const entries = portfolioQueue.listQueue(parsed.repoFullName); - if (parsed.json) { - console.log(JSON.stringify({ entries }, null, 2)); - } - else { - console.log(renderQueueTable(entries)); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runQueueNext(args, options = {}) { - const parsed = parseQueueNextArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - const capsRequested = parsed.globalWipCap !== undefined || parsed.perRepoWipCap !== undefined; - if (parsed.dryRun) { - const dryRunResult = capsRequested - ? { outcome: "dry_run", globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap } - : { outcome: "dry_run" }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else if (capsRequested) { - console.log(`DRY RUN: would dequeue the highest-priority queued item within WIP caps (global-wip: ${parsed.globalWipCap ?? "unset"}, per-repo-wip: ${parsed.perRepoWipCap ?? "unset"}). No portfolio-queue write was made.`); - } - else { - console.log("DRY RUN: would dequeue the highest-priority queued item. No portfolio-queue write was made."); - } - return 0; - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - let entry; - if (capsRequested) { - // Unset dimensions stay genuinely uncapped (Infinity), not silently defaulted to 1 like claim-batch. - const caps = { - globalWipCap: parsed.globalWipCap ?? Number.POSITIVE_INFINITY, - perRepoWipCap: parsed.perRepoWipCap ?? Number.POSITIVE_INFINITY, - }; - const claimed = portfolioQueue.batchClaim((entries) => selectNextEligibleTarget(entries, caps)); - entry = claimed[0] ?? null; - } - else { - entry = portfolioQueue.dequeueNext(); - } - if (parsed.json) { - console.log(JSON.stringify({ entry }, null, 2)); - } - else { - console.log(entry ? entry.identifier : "none"); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runQueueDone(args, options = {}) { - const parsed = parseQueueDoneArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would mark ${parsed.repoFullName} ${parsed.identifier} done. No portfolio-queue write was made.`); - } - return 0; - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - const entry = portfolioQueue.markDone(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl); - if (!entry) { - return reportCliFailure(parsed.json, "queue_entry_not_found"); - } - if (parsed.json) { - console.log(JSON.stringify({ entry }, null, 2)); - } - else { - console.log(entry.status); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -/** `release `: manually give up a CLAIMED (in_progress) item, returning it to the queue - * (the manual counterpart to the automated stuck-lease sweep). Exit 2 when there is no in-flight item to release. */ -export function runQueueRelease(args, options = {}) { - const parsed = parseQueueReleaseArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would release ${parsed.repoFullName} ${parsed.identifier} back to the queue. No portfolio-queue write was made.`); - } - return 0; - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - const entry = portfolioQueue.reclaimStuckItem(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl); - if (!entry) { - return reportCliFailure(parsed.json, "queue_entry_not_in_progress"); - } - if (parsed.json) { - console.log(JSON.stringify({ entry }, null, 2)); - } - else { - console.log(entry.status); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -/** `requeue `: manually put a COMPLETED (done) item back on the queue so it is picked up - * again, keeping its original FIFO position. Exit 2 when there is no done item to requeue (already queued, - * in-flight — release it instead — or absent). */ -export function runQueueRequeue(args, options = {}) { - const parsed = parseQueueRequeueArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would requeue ${parsed.repoFullName} ${parsed.identifier}. No portfolio-queue write was made.`); - } - return 0; - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - const entry = portfolioQueue.requeueItem(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl); - if (!entry) { - return reportCliFailure(parsed.json, "queue_entry_not_requeuable"); - } - if (parsed.json) { - console.log(JSON.stringify({ entry }, null, 2)); - } - else { - console.log(entry.status); - } - return 0; - }); - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function parseQueueClaimBatchArgs(args) { - const options = { - json: false, - dryRun: false, - globalWipCap: 1, - perRepoWipCap: 1, - }; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--global-wip" || token === "--per-repo-wip") { - const value = Number(args[index + 1]); - if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) { - return { error: QUEUE_CLAIM_BATCH_USAGE }; - } - if (token === "--global-wip") - options.globalWipCap = value; - else - options.perRepoWipCap = value; - index += 1; - continue; - } - return { error: QUEUE_CLAIM_BATCH_USAGE }; - } - return options; -} -/** Claim the next caps-aware batch via the WIP-cap-aware batch claimer (portfolio-queue-manager.js), which also - * reclaims any leases orphaned by a crashed process first (#4833 wires the previously caller-less claimer). */ -export function runQueueClaimBatch(args, options = {}) { - const parsed = parseQueueClaimBatchArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } - else { - console.log(`DRY RUN: would claim a batch (global-wip: ${parsed.globalWipCap}, per-repo-wip: ${parsed.perRepoWipCap}). No portfolio-queue write was made.`); - } - return 0; - } - // Open the manager INSIDE the try so a store open failure returns 2 instead of crashing; the finally guards the - // close with `?.` since the initializer may have thrown before assigning. - const ownsManager = options.initPortfolioQueueManager === undefined; - let manager; - try { - manager = (options.initPortfolioQueueManager ?? initPortfolioQueueManager)({ - caps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap }, - }); - const claimed = manager.claimNextBatch(); - if (parsed.json) { - console.log(JSON.stringify({ claimed }, null, 2)); - } - else { - console.log(claimed.length === 0 ? "none" : claimed.map((entry) => entry.identifier).join("\n")); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } - finally { - if (ownsManager) - manager?.close(); - } -} -const QUEUE_METRICS_USAGE = "Usage: loopover-miner queue metrics"; -// Prometheus metric names for the portfolio-queue gauges (#5186). Mirrors the `loopover_miner_*` naming and -// HELP/TYPE/label conventions of event-ledger-cli.js's renderEventLedgerMetrics / the engine's -// renderMinerPredictionMetrics, rather than importing across the package boundary. -export const QUEUE_ITEMS = "loopover_miner_portfolio_queue_items"; -export const QUEUE_OLDEST_IN_PROGRESS_LEASE_AGE_SECONDS = "loopover_miner_portfolio_queue_oldest_in_progress_lease_age_seconds"; -/** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */ -function escapeMetricsHelpText(help) { - return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); -} -/** - * Render portfolio-queue backlog health as Prometheus text-exposition gauges: current item count per status, and - * the age of the OLDEST still-in-flight lease -- the concrete "is anything stuck" signal a - * `loopover_queue_oldest_maintenance_pending_age_seconds`-style alert rule can threshold on (#5186). Pure and - * side-effect-free: the caller supplies the rows and `nowMs` (no internal clock read, matching - * store-maintenance.js's pruneLedgerByRetention convention) and prints the result. Deterministic (status series - * sorted); always emits HELP/TYPE so an empty queue is still a well-formed exposition document, and the lease-age - * gauge reads 0 (never stuck) rather than being omitted when nothing is in-flight. - * @param {Array<{ status: string }>} queueEntries - every row, any status (e.g. store.listQueue()'s output). - * @param {Array<{ leasedAt: string | null }>} leaseEntries - in-flight rows only (store.listInProgress()'s output). - * @param {number} nowMs - */ -export function renderPortfolioQueueMetrics(queueEntries, leaseEntries, nowMs) { - const countByStatus = new Map(); - for (const entry of queueEntries) { - countByStatus.set(entry.status, (countByStatus.get(entry.status) ?? 0) + 1); - } - let oldestLeaseAgeSeconds = 0; - for (const lease of leaseEntries) { - const leasedAtMs = Date.parse(lease.leasedAt ?? ""); - if (!Number.isFinite(leasedAtMs)) - continue; - const ageSeconds = Math.max(0, (nowMs - leasedAtMs) / 1000); - if (ageSeconds > oldestLeaseAgeSeconds) - oldestLeaseAgeSeconds = ageSeconds; - } - const lines = [ - `# HELP ${QUEUE_ITEMS} ${escapeMetricsHelpText("Current portfolio-queue item count, by status.")}`, - `# TYPE ${QUEUE_ITEMS} gauge`, - ]; - for (const [status, count] of [...countByStatus.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { - lines.push(`${QUEUE_ITEMS}{status="${status}"} ${count}`); - } - lines.push(`# HELP ${QUEUE_OLDEST_IN_PROGRESS_LEASE_AGE_SECONDS} ${escapeMetricsHelpText("Age in seconds of the oldest still-in-flight (in_progress) claim lease. 0 when nothing is in-flight.")}`); - lines.push(`# TYPE ${QUEUE_OLDEST_IN_PROGRESS_LEASE_AGE_SECONDS} gauge`); - lines.push(`${QUEUE_OLDEST_IN_PROGRESS_LEASE_AGE_SECONDS} ${oldestLeaseAgeSeconds}`); - return `${lines.join("\n")}\n`; -} -export function runQueueMetrics(args, options = {}) { - if (args.length > 0) { - return reportCliFailure(argsWantJson(args), QUEUE_METRICS_USAGE); - } - try { - return withPortfolioQueue(options, (portfolioQueue) => { - const nowMs = typeof options.nowMs === "number" && Number.isFinite(options.nowMs) ? options.nowMs : Date.now(); - // renderPortfolioQueueMetrics returns a newline-terminated document; console.log re-adds the terminator, so - // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics). - console.log(renderPortfolioQueueMetrics(portfolioQueue.listQueue(), portfolioQueue.listInProgress(), nowMs).trimEnd()); - return 0; - }); - } - catch (error) { - return reportCliFailure(argsWantJson(args), describeCliError(error)); - } -} -export function runQueueCli(subcommand, args, options = {}) { - if (subcommand === "list") - return runQueueList(args, options); - if (subcommand === "next") - return runQueueNext(args, options); - if (subcommand === "done") - return runQueueDone(args, options); - if (subcommand === "release") - return runQueueRelease(args, options); - if (subcommand === "requeue") - return runQueueRequeue(args, options); - if (subcommand === "claim-batch") - return runQueueClaimBatch(args, options); - if (subcommand === "metrics") - return runQueueMetrics(args, options); - if (subcommand === "dashboard") - return runPortfolioDashboard(args, options); - return reportCliFailure(argsWantJson(args), `Unknown queue subcommand: ${subcommand ?? ""}. ${QUEUE_LIST_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLXF1ZXVlLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBvcnRmb2xpby1xdWV1ZS1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFFL0QsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFFekUsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakUsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2xGLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRXJELE1BQU0sZ0JBQWdCLEdBQUcsaUVBQWlFLENBQUM7QUFDM0YsTUFBTSxnQkFBZ0IsR0FDcEIsK0ZBQStGLENBQUM7QUFDbEcsTUFBTSxnQkFBZ0IsR0FDcEIsd0dBQXdHLENBQUM7QUFDM0csTUFBTSxtQkFBbUIsR0FDdkIsMkdBQTJHLENBQUM7QUFDOUcsTUFBTSxtQkFBbUIsR0FDdkIsMkdBQTJHLENBQUM7QUFDOUcsTUFBTSx1QkFBdUIsR0FDM0Isc0dBQXNHLENBQUM7QUFtQ3pHLFNBQVMsWUFBWSxDQUFDLEtBQXlCLEVBQUUsS0FBYTtJQUM1RCxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDcEMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzdCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDM0MsT0FBTyxFQUFFLEtBQUss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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-queue-expiry.d.ts b/packages/loopover-miner/lib/portfolio-queue-expiry.d.ts deleted file mode 100644 index 517f51a59d..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-expiry.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** PURE — no IO, no Date, no random (#4827). Mirror of claim-ledger-expiry.js for the portfolio-queue store: a - * crashed/killed process leaves its item stuck 'in_progress' forever, so sweep leases older than a bound back to - * 'queued'. */ -import type { QueueEntry, QueueLeaseEntry } from "./portfolio-queue.js"; -export declare const DEFAULT_MAX_LEASE_MS: number; -export type PortfolioQueueExpiryStore = { - listInProgress(): QueueLeaseEntry[]; - reclaimStuckItem(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; -}; -/** - * Return in-flight items whose lease age is strictly greater than `maxLeaseMs`. An item whose age equals - * `maxLeaseMs` exactly is still within the window (not stuck). Items that are not 'in_progress', or whose - * `leasedAt` is missing/unparseable, are never returned. - */ -export declare function findStuckItems(items: QueueLeaseEntry[], nowMs: number, maxLeaseMs: number): QueueLeaseEntry[]; -/** - * Reclaim every stuck in-flight item back to 'queued', returning the reclaimed entries. `store.listInProgress()` - * supplies the lease-annotated rows and `store.reclaimStuckItem()` performs the atomic per-item flip — the same - * store/sweep split sweepExpiredClaims uses. - */ -export declare function sweepStuckItems(store: PortfolioQueueExpiryStore, nowMs: number, maxLeaseMs?: number): QueueEntry[]; diff --git a/packages/loopover-miner/lib/portfolio-queue-expiry.js b/packages/loopover-miner/lib/portfolio-queue-expiry.js deleted file mode 100644 index 7a679aea93..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-expiry.js +++ /dev/null @@ -1,55 +0,0 @@ -/** PURE — no IO, no Date, no random (#4827). Mirror of claim-ledger-expiry.js for the portfolio-queue store: a - * crashed/killed process leaves its item stuck 'in_progress' forever, so sweep leases older than a bound back to - * 'queued'. */ -// A generous default: a real attempt rarely holds a single portfolio item for long, so 30 minutes without the row -// leaving 'in_progress' strongly implies the owning process died rather than that it is still working. -export const DEFAULT_MAX_LEASE_MS = 30 * 60 * 1000; -function leaseAgeMs(item, nowMs) { - const leasedAtMs = Date.parse(item.leasedAt); - if (!Number.isFinite(leasedAtMs)) - return null; - return nowMs - leasedAtMs; -} -/** - * Return in-flight items whose lease age is strictly greater than `maxLeaseMs`. An item whose age equals - * `maxLeaseMs` exactly is still within the window (not stuck). Items that are not 'in_progress', or whose - * `leasedAt` is missing/unparseable, are never returned. - */ -export function findStuckItems(items, nowMs, maxLeaseMs) { - if (!Number.isFinite(nowMs) || nowMs < 0) - throw new Error("invalid_now_ms"); - if (!Number.isFinite(maxLeaseMs) || maxLeaseMs < 0) - throw new Error("invalid_max_lease_ms"); - if (!Array.isArray(items)) - throw new Error("invalid_items"); - const stuck = []; - for (const item of items) { - if (item?.status !== "in_progress") - continue; - const ageMs = leaseAgeMs(item, nowMs); - if (ageMs === null) - continue; - if (ageMs > maxLeaseMs) - stuck.push(item); - } - return stuck; -} -/** - * Reclaim every stuck in-flight item back to 'queued', returning the reclaimed entries. `store.listInProgress()` - * supplies the lease-annotated rows and `store.reclaimStuckItem()` performs the atomic per-item flip — the same - * store/sweep split sweepExpiredClaims uses. - */ -export function sweepStuckItems(store, nowMs, maxLeaseMs = DEFAULT_MAX_LEASE_MS) { - const inProgress = store.listInProgress(); - const stuck = findStuckItems(inProgress, nowMs, maxLeaseMs); - const reclaimed = []; - for (const item of stuck) { - // Echo the item's OWN apiBaseUrl back (#5563) rather than defaulting: two forge hosts can each have an - // in-flight item with the same owner/repo+identifier, and defaulting here would reclaim the wrong host's row. - const updated = store.reclaimStuckItem(item.repoFullName, item.identifier, item.apiBaseUrl); - if (updated) - reclaimed.push(updated); - } - return reclaimed; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLXF1ZXVlLWV4cGlyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInBvcnRmb2xpby1xdWV1ZS1leHBpcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O2dCQUVnQjtBQUloQixrSEFBa0g7QUFDbEgsdUdBQXVHO0FBQ3ZHLE1BQU0sQ0FBQyxNQUFNLG9CQUFvQixHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDO0FBT25ELFNBQVMsVUFBVSxDQUFDLElBQXFCLEVBQUUsS0FBYTtJQUN0RCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxRQUFrQixDQUFDLENBQUM7SUFDdkQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDOUMsT0FBTyxLQUFLLEdBQUcsVUFBVSxDQUFDO0FBQzVCLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLGNBQWMsQ0FBQyxLQUF3QixFQUFFLEtBQWEsRUFBRSxVQUFrQjtJQUN4RixJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLEdBQUcsQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztJQUM1RSxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxVQUFVLEdBQUcsQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUM1RixJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGVBQWUsQ0FBQyxDQUFDO0lBRTVELE1BQU0sS0FBSyxHQUFzQixFQUFFLENBQUM7SUFDcEMsS0FBSyxNQUFNLElBQUksSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUN6QixJQUFJLElBQUksRUFBRSxNQUFNLEtBQUssYUFBYTtZQUFFLFNBQVM7UUFDN0MsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztRQUN0QyxJQUFJLEtBQUssS0FBSyxJQUFJO1lBQUUsU0FBUztRQUM3QixJQUFJLEtBQUssR0FBRyxVQUFVO1lBQUUsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMzQyxDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUM7QUFDZixDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQzdCLEtBQWdDLEVBQ2hDLEtBQWEsRUFDYixhQUFxQixvQkFBb0I7SUFFekMsTUFBTSxVQUFVLEdBQUcsS0FBSyxDQUFDLGNBQWMsRUFBRSxDQUFDO0lBQzFDLE1BQU0sS0FBSyxHQUFHLGNBQWMsQ0FBQyxVQUFVLEVBQUUsS0FBSyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQzVELE1BQU0sU0FBUyxHQUFpQixFQUFFLENBQUM7SUFDbkMsS0FBSyxNQUFNLElBQUksSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUN6Qix1R0FBdUc7UUFDdkcsOEdBQThHO1FBQzlHLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxnQkFBZ0IsQ0FBQyxJQUFJLENBQUMsWUFBWSxFQUFFLElBQUksQ0FBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQzVGLElBQUksT0FBTztZQUFFLFNBQVMsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdkMsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-queue-manager.d.ts b/packages/loopover-miner/lib/portfolio-queue-manager.d.ts deleted file mode 100644 index 600e746857..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-manager.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { PortfolioCaps } from "@loopover/engine"; -import type { EnqueueItem, PortfolioQueueStore, QueueEntry } from "./portfolio-queue.js"; -export type PortfolioQueueClaimTarget = { - apiBaseUrl: string; - repoFullName: string; - identifier: string; -}; -/** - * Stable composite id for projecting SQLite rows into the engine's PortfolioQueueItem shape. Encodes apiBaseUrl - * too (#5563) — the engine's own selection logic has no forge dimension, but two hosts can now enqueue an item - * under the same repoFullName+identifier (post-#5563 scoping), and the id is the ONLY thing selectEligibleBatch's - * output threads back to batchClaim; without the host baked in here, a selected item's host would be lost and - * batchClaim would default to github.com, potentially claiming a DIFFERENT row than the one the engine selected. - */ -export declare function queueItemId(apiBaseUrl: string, repoFullName: string, identifier: string): string; -/** Reverse {@link queueItemId} after engine selection so claims can target SQLite primary keys. */ -export declare function parseQueueItemId(id: unknown): PortfolioQueueClaimTarget; -/** Coerce caps to finite non-negative integers (mirrors the engine's normalizeCaps posture). */ -export declare function normalizePortfolioCaps(caps?: Partial): PortfolioCaps; -/** Project persisted queue rows into the engine's in-memory PortfolioQueue (done rows omitted). Pure. */ -export declare function entriesToPortfolioQueue(entries: QueueEntry[]): { - buckets: Array<{ - repoFullName: string; - items: Array<{ - id: string; - repoFullName: string; - state: "queued" | "in_progress"; - }>; - }>; -}; -/** Select the next eligible batch from active rows using the engine primitive. Pure. */ -export declare function selectEligibleBatch(entries: QueueEntry[], caps: PortfolioCaps): PortfolioQueueClaimTarget[]; -export type PortfolioQueueManager = { - caps: PortfolioCaps; - store: PortfolioQueueStore; - dbPath: string; - enqueue(item: EnqueueItem): QueueEntry; - listQueue(repoFullName?: string | null): QueueEntry[]; - markDone(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - markFailed(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - reclaimStuckItems(maxLeaseMs?: number): QueueEntry[]; - claimNextBatch(): QueueEntry[]; - close(): void; -}; -export type InitPortfolioQueueManagerOptions = { - caps?: Partial; - store?: PortfolioQueueStore; - dbPath?: string; - staleLeaseMs?: number; -}; -/** - * Open a caps-aware portfolio queue manager backed by the local SQLite store. The existing single-row - * `dequeueNext()` CLI surface is untouched — this adds `claimNextBatch()` for fleet-style batch claiming. - */ -export declare function initPortfolioQueueManager(options?: InitPortfolioQueueManagerOptions): PortfolioQueueManager; -export declare function closeDefaultPortfolioQueueManager(): void; diff --git a/packages/loopover-miner/lib/portfolio-queue-manager.js b/packages/loopover-miner/lib/portfolio-queue-manager.js deleted file mode 100644 index ccfcdfcefe..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue-manager.js +++ /dev/null @@ -1,146 +0,0 @@ -// Stateful PortfolioQueueManager (#4285): compose the persisted SQLite portfolio/queue store -// (portfolio-queue.js, #2292) with the pure engine selector (nextEligibleItems, queue.ts, #2326) so batch -// claiming respects global/per-repo WIP caps and cross-repo diversification instead of a naive priority-only -// single-row dequeue. Caps are plain constructor arguments — not wired to .loopover-miner.yml here. -import { nextEligibleItems } from "@loopover/engine"; -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { initPortfolioQueueStore } from "./portfolio-queue.js"; -import { DEFAULT_MAX_LEASE_MS, sweepStuckItems } from "./portfolio-queue-expiry.js"; -const ITEM_ID_SEPARATOR = "::"; -/** - * Stable composite id for projecting SQLite rows into the engine's PortfolioQueueItem shape. Encodes apiBaseUrl - * too (#5563) — the engine's own selection logic has no forge dimension, but two hosts can now enqueue an item - * under the same repoFullName+identifier (post-#5563 scoping), and the id is the ONLY thing selectEligibleBatch's - * output threads back to batchClaim; without the host baked in here, a selected item's host would be lost and - * batchClaim would default to github.com, potentially claiming a DIFFERENT row than the one the engine selected. - */ -export function queueItemId(apiBaseUrl, repoFullName, identifier) { - return `${apiBaseUrl}${ITEM_ID_SEPARATOR}${repoFullName}${ITEM_ID_SEPARATOR}${identifier}`; -} -/** Reverse {@link queueItemId} after engine selection so claims can target SQLite primary keys. */ -export function parseQueueItemId(id) { - if (typeof id !== "string") - throw new Error("invalid_queue_item_id"); - const lastSeparatorIndex = id.lastIndexOf(ITEM_ID_SEPARATOR); - if (lastSeparatorIndex <= 0) - throw new Error("invalid_queue_item_id"); - const identifier = id.slice(lastSeparatorIndex + ITEM_ID_SEPARATOR.length); - if (!identifier) - throw new Error("invalid_queue_item_id"); - const beforeIdentifier = id.slice(0, lastSeparatorIndex); - const secondLastSeparatorIndex = beforeIdentifier.lastIndexOf(ITEM_ID_SEPARATOR); - if (secondLastSeparatorIndex <= 0) - throw new Error("invalid_queue_item_id"); - const repoFullName = beforeIdentifier.slice(secondLastSeparatorIndex + ITEM_ID_SEPARATOR.length); - if (!repoFullName) - throw new Error("invalid_queue_item_id"); - const apiBaseUrl = beforeIdentifier.slice(0, secondLastSeparatorIndex); - // Unreachable at runtime: secondLastSeparatorIndex > 0 (guarded above), so slice(0, >0) is always non-empty -- - // this guard has no reachable input. Kept as defense-in-depth mirroring the other segment checks. - /* v8 ignore next -- unreachable: secondLastSeparatorIndex > 0 guarantees a non-empty apiBaseUrl slice */ - if (!apiBaseUrl) - throw new Error("invalid_queue_item_id"); - return { apiBaseUrl, repoFullName, identifier }; -} -/** Coerce caps to finite non-negative integers (mirrors the engine's normalizeCaps posture). */ -export function normalizePortfolioCaps(caps = {}) { - const globalWipCap = Number.isFinite(caps.globalWipCap) ? Math.max(0, Math.trunc(caps.globalWipCap)) : 0; - const perRepoWipCap = Number.isFinite(caps.perRepoWipCap) ? Math.max(0, Math.trunc(caps.perRepoWipCap)) : 0; - return { globalWipCap, perRepoWipCap }; -} -/** Project persisted queue rows into the engine's in-memory PortfolioQueue (done rows omitted). Pure. */ -export function entriesToPortfolioQueue(entries) { - const activeEntries = Array.isArray(entries) ? entries.filter((entry) => entry?.status !== "done") : []; - const bucketsByRepo = new Map(); - const bucketOrder = []; - for (const entry of activeEntries) { - const repoFullName = typeof entry.repoFullName === "string" ? entry.repoFullName.trim() : ""; - const identifier = typeof entry.identifier === "string" ? entry.identifier.trim() : ""; - if (!repoFullName || !identifier) - continue; - // Falls back to the github.com default (matching every store's own normalizeApiBaseUrl) so a row from - // before #5563 threaded apiBaseUrl through this fold still gets a valid, host-scoped id. - const apiBaseUrl = typeof entry.apiBaseUrl === "string" && entry.apiBaseUrl.trim() ? entry.apiBaseUrl.trim() : DEFAULT_FORGE_CONFIG.apiBaseUrl; - // Host-qualify the engine's per-repo WIP grouping key (#7224). nextEligibleItems groups its per-repo cap by each - // item's `repoFullName`, which it treats as an OPAQUE string -- the engine has no apiBaseUrl concept, the host is - // smuggled through the opaque `id` (queueItemId, #5563). The store keys rows by apiBaseUrl too, so two forge - // hosts' same-named repos are distinct backlogs; without qualifying the grouping key by host here, a per-repo cap - // was shared across them (e.g. perRepoWipCap: 1 let only ONE host's backlog advance). The `id` still carries the - // TRUE repoFullName and selectEligibleBatch maps results back via parseQueueItemId(id), so the real repo/host - // survive to the caller. Single-host behavior is unchanged: one apiBaseUrl means one grouping key per repo. - const repoLower = repoFullName.toLowerCase(); - const repoKey = `${apiBaseUrl}\n${repoLower}`; - if (!bucketsByRepo.has(repoKey)) { - // The bucket's own repoFullName stays the plain repo (display/diversification), while each ITEM carries the - // host-qualified key the engine groups on -- so the returned bucket shape is unchanged for single-host. - bucketsByRepo.set(repoKey, { repoFullName: repoLower, items: [] }); - bucketOrder.push(repoKey); - } - bucketsByRepo.get(repoKey).items.push({ - id: queueItemId(apiBaseUrl, repoFullName, identifier), - repoFullName: repoKey, - state: entry.status === "in_progress" ? "in_progress" : "queued", - }); - } - return { - buckets: bucketOrder.map((repoKey) => { - const bucket = bucketsByRepo.get(repoKey); - return { repoFullName: bucket.repoFullName, items: bucket.items }; - }), - }; -} -/** Select the next eligible batch from active rows using the engine primitive. Pure. */ -export function selectEligibleBatch(entries, caps) { - const normalizedCaps = normalizePortfolioCaps(caps); - const queue = entriesToPortfolioQueue(entries); - return nextEligibleItems(queue, normalizedCaps).map((item) => parseQueueItemId(item.id)); -} -/** - * Open a caps-aware portfolio queue manager backed by the local SQLite store. The existing single-row - * `dequeueNext()` CLI surface is untouched — this adds `claimNextBatch()` for fleet-style batch claiming. - */ -export function initPortfolioQueueManager(options = {}) { - const caps = normalizePortfolioCaps(options.caps ?? { globalWipCap: 1, perRepoWipCap: 1 }); - const store = options.store ?? initPortfolioQueueStore(options.dbPath); - // A lease older than this means the process that claimed the item almost certainly died; the item is swept back - // to 'queued' so it no longer occupies WIP capacity forever (#4827). - const staleLeaseMs = Number.isFinite(options.staleLeaseMs) ? options.staleLeaseMs : DEFAULT_MAX_LEASE_MS; - return { - caps, - store, - dbPath: store.dbPath, - enqueue(item) { - return store.enqueue(item); - }, - listQueue(repoFullName) { - return store.listQueue(repoFullName); - }, - markDone(repoFullName, identifier, apiBaseUrl) { - return store.markDone(repoFullName, identifier, apiBaseUrl); - }, - markFailed(repoFullName, identifier, apiBaseUrl) { - return store.markFailed(repoFullName, identifier, apiBaseUrl); - }, - /** Sweep leases orphaned by a crashed/killed process back to 'queued', returning the reclaimed items (#4827). */ - reclaimStuckItems(maxLeaseMs = staleLeaseMs) { - return sweepStuckItems(store, Date.now(), maxLeaseMs); - }, - // The engine primitive itself (@loopover/engine's nextEligibleItems) has no apiBaseUrl concept -- - // it only ever sees the opaque `id` string. queueItemId/parseQueueItemId (#5563) smuggle the host through - // that id round-trip, so selectFn's output below correctly carries each selected item's OWN apiBaseUrl into - // batchClaim, instead of every claim defaulting to github.com regardless of which host's row was selected. - claimNextBatch() { - // Reclaim orphaned leases first, so an item stranded 'in_progress' by a dead process becomes eligible again - // instead of permanently consuming a WIP slot and starving the queue. - sweepStuckItems(store, Date.now(), staleLeaseMs); - return store.batchClaim((entries) => selectEligibleBatch(entries, caps)); - }, - close() { - store.close(); - }, - }; -} -export function closeDefaultPortfolioQueueManager() { - // Reserved for symmetry with other miner stores; managers are opened explicitly today. -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLXF1ZXVlLW1hbmFnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwb3J0Zm9saW8tcXVldWUtbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw2RkFBNkY7QUFDN0YsMEdBQTBHO0FBQzFHLDZHQUE2RztBQUM3RyxvR0FBb0c7QUFDcEcsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFckQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDekQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFFL0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLGVBQWUsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBRXBGLE1BQU0saUJBQWlCLEdBQUcsSUFBSSxDQUFDO0FBUS9COzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxXQUFXLENBQUMsVUFBa0IsRUFBRSxZQUFvQixFQUFFLFVBQWtCO0lBQ3RGLE9BQU8sR0FBRyxVQUFVLEdBQUcsaUJBQWlCLEdBQUcsWUFBWSxHQUFHLGlCQUFpQixHQUFHLFVBQVUsRUFBRSxDQUFDO0FBQzdGLENBQUM7QUFFRCxtR0FBbUc7QUFDbkcsTUFBTSxVQUFVLGdCQUFnQixDQUFDLEVBQVc7SUFDMUMsSUFBSSxPQUFPLEVBQUUsS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sa0JBQWtCLEdBQUcsRUFBRSxDQUFDLFdBQVcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQzdELElBQUksa0JBQWtCLElBQUksQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLENBQUMsQ0FBQztJQUN0RSxNQUFNLFVBQVUsR0FBRyxFQUFFLENBQUMsS0FBSyxDQUFDLGtCQUFrQixHQUFHLGlCQUFpQixDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQzNFLElBQUksQ0FBQyxVQUFVO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO0lBQzFELE1BQU0sZ0JBQWdCLEdBQUcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztJQUN6RCxNQUFNLHdCQUF3QixHQUFHLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ2pGLElBQUksd0JBQXdCLElBQUksQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLENBQUMsQ0FBQztJQUM1RSxNQUFNLFlBQVksR0FBRyxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsd0JBQXdCLEdBQUcsaUJBQWlCLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDakcsSUFBSSxDQUFDLFlBQVk7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHVCQUF1QixDQUFDLENBQUM7SUFDNUQsTUFBTSxVQUFVLEdBQUcsZ0JBQWdCLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSx3QkFBd0IsQ0FBQyxDQUFDO0lBQ3ZFLCtHQUErRztJQUMvRyxrR0FBa0c7SUFDbEcseUdBQXlHO0lBQ3pHLElBQUksQ0FBQyxVQUFVO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO0lBQzFELE9BQU8sRUFBRSxVQUFVLEVBQUUsWUFBWSxFQUFFLFVBQVUsRUFBRSxDQUFDO0FBQ2xELENBQUM7QUFFRCxnR0FBZ0c7QUFDaEcsTUFBTSxVQUFVLHNCQUFzQixDQUFDLE9BQStCLEVBQUU7SUFDdEUsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFlBQXNCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDbkgsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLGFBQXVCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdEgsT0FBTyxFQUFFLFlBQVksRUFBRSxhQUFhLEVBQUUsQ0FBQztBQUN6QyxDQUFDO0FBRUQseUdBQXlHO0FBQ3pHLE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxPQUFxQjtJQU0zRCxNQUFNLGFBQWEsR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLEVBQUUsTUFBTSxLQUFLLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDeEcsTUFBTSxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBRzFCLENBQUM7SUFDSixNQUFNLFdBQVcsR0FBYSxFQUFFLENBQUM7SUFDakMsS0FBSyxNQUFNLEtBQUssSUFBSSxhQUFhLEVBQUUsQ0FBQztRQUNsQyxNQUFNLFlBQVksR0FBRyxPQUFPLEtBQUssQ0FBQyxZQUFZLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDN0YsTUFBTSxVQUFVLEdBQUcsT0FBTyxLQUFLLENBQUMsVUFBVSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ3ZGLElBQUksQ0FBQyxZQUFZLElBQUksQ0FBQyxVQUFVO1lBQUUsU0FBUztRQUMzQyxzR0FBc0c7UUFDdEcseUZBQXlGO1FBQ3pGLE1BQU0sVUFBVSxHQUFHLE9BQU8sS0FBSyxDQUFDLFVBQVUsS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsb0JBQW9CLENBQUMsVUFBVSxDQUFDO1FBQy9JLGlIQUFpSDtRQUNqSCxrSEFBa0g7UUFDbEgsNkdBQTZHO1FBQzdHLGtIQUFrSDtRQUNsSCxpSEFBaUg7UUFDakgsOEdBQThHO1FBQzlHLDRHQUE0RztRQUM1RyxNQUFNLFNBQVMsR0FBRyxZQUFZLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDN0MsTUFBTSxPQUFPLEdBQUcsR0FBRyxVQUFVLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDOUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNoQyw0R0FBNEc7WUFDNUcsd0dBQXdHO1lBQ3hHLGFBQWEsQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEVBQUUsWUFBWSxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNuRSxXQUFXLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzVCLENBQUM7UUFDRCxhQUFhLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUM7WUFDckMsRUFBRSxFQUFFLFdBQVcsQ0FBQyxVQUFVLEVBQUUsWUFBWSxFQUFFLFVBQVUsQ0FBQztZQUNyRCxZQUFZLEVBQUUsT0FBTztZQUNyQixLQUFLLEVBQUUsS0FBSyxDQUFDLE1BQU0sS0FBSyxhQUFhLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsUUFBUTtTQUNqRSxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTztRQUNMLE9BQU8sRUFBRSxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUU7WUFDbkMsTUFBTSxNQUFNLEdBQUcsYUFBYSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUUsQ0FBQztZQUMzQyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sQ0FBQyxZQUFZLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNwRSxDQUFDLENBQUM7S0FDSCxDQUFDO0FBQ0osQ0FBQztBQUVELHdGQUF3RjtBQUN4RixNQUFNLFVBQVUsbUJBQW1CLENBQUMsT0FBcUIsRUFBRSxJQUFtQjtJQUM1RSxNQUFNLGNBQWMsR0FBRyxzQkFBc0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNwRCxNQUFNLEtBQUssR0FBRyx1QkFBdUIsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUMvQyxPQUFPLGlCQUFpQixDQUFDLEtBQUssRUFBRSxjQUFjLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBQzNGLENBQUM7QUFzQkQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLHlCQUF5QixDQUFDLFVBQTRDLEVBQUU7SUFDdEYsTUFBTSxJQUFJLEdBQUcsc0JBQXNCLENBQUMsT0FBTyxDQUFDLElBQUksSUFBSSxFQUFFLFlBQVksRUFBRSxDQUFDLEVBQUUsYUFBYSxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDM0YsTUFBTSxLQUFLLEdBQUcsT0FBTyxDQUFDLEtBQUssSUFBSSx1QkFBdUIsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDdkUsZ0hBQWdIO0lBQ2hILHFFQUFxRTtJQUNyRSxNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUUsT0FBTyxDQUFDLFlBQXVCLENBQUMsQ0FBQyxDQUFDLG9CQUFvQixDQUFDO0lBRXJILE9BQU87UUFDTCxJQUFJO1FBQ0osS0FBSztRQUNMLE1BQU0sRUFBRSxLQUFLLENBQUMsTUFBTTtRQUNwQixPQUFPLENBQUMsSUFBSTtZQUNWLE9BQU8sS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM3QixDQUFDO1FBQ0QsU0FBUyxDQUFDLFlBQVk7WUFDcEIsT0FBTyxLQUFLLENBQUMsU0FBUyxDQUFDLFlBQVksQ0FBQyxDQUFDO1FBQ3ZDLENBQUM7UUFDRCxRQUFRLENBQUMsWUFBWSxFQUFFLFVBQVUsRUFBRSxVQUFVO1lBQzNDLE9BQU8sS0FBSyxDQUFDLFFBQVEsQ0FBQyxZQUFZLEVBQUUsVUFBVSxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQzlELENBQUM7UUFDRCxVQUFVLENBQUMsWUFBWSxFQUFFLFVBQVUsRUFBRSxVQUFVO1lBQzdDLE9BQU8sS0FBSyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsVUFBVSxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ2hFLENBQUM7UUFDRCxpSEFBaUg7UUFDakgsaUJBQWlCLENBQUMsVUFBVSxHQUFHLFlBQVk7WUFDekMsT0FBTyxlQUFlLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsRUFBRSxVQUFVLENBQUMsQ0FBQztRQUN4RCxDQUFDO1FBQ0Qsa0dBQWtHO1FBQ2xHLDBHQUEwRztRQUMxRyw0R0FBNEc7UUFDNUcsMkdBQTJHO1FBQzNHLGNBQWM7WUFDWiw0R0FBNEc7WUFDNUcsc0VBQXNFO1lBQ3RFLGVBQWUsQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRSxFQUFFLFlBQVksQ0FBQyxDQUFDO1lBQ2pELE9BQU8sS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsbUJBQW1CLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDM0UsQ0FBQztRQUNELEtBQUs7WUFDSCxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDaEIsQ0FBQztLQUNGLENBQUM7QUFDSixDQUFDO0FBRUQsTUFBTSxVQUFVLGlDQUFpQztJQUMvQyx1RkFBdUY7QUFDekYsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/portfolio-queue.d.ts b/packages/loopover-miner/lib/portfolio-queue.d.ts deleted file mode 100644 index 43cc58edd3..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type QueueStatus = "queued" | "in_progress" | "done"; -export type QueueEntry = { - apiBaseUrl: string; - repoFullName: string; - identifier: string; - priority: number; - status: QueueStatus; - enqueuedAt: string; -}; -export type EnqueueItem = { - repoFullName: string; - identifier: string; - priority?: number | null; - apiBaseUrl?: string; -}; -/** Lease-annotated view of an in-flight row: when it was claimed, for the expiry sweep (#4827). */ -export type QueueLeaseEntry = { - apiBaseUrl: string; - repoFullName: string; - identifier: string; - status: QueueStatus; - leasedAt: string | null; -}; -/** A real per-item PortfolioConvergenceInput (non-convergence.ts, #5654), read from this store's own - * attempt-history counters -- see getAttemptHistory. */ -export type QueueAttemptHistory = { - attempts: number; - consecutiveFailures: number; - reenqueues: number; - reachedDone: boolean; -}; -export type PortfolioQueueStore = { - dbPath: string; - enqueue(item: EnqueueItem): QueueEntry; - dequeueNext(): QueueEntry | null; - listQueue(repoFullName?: string | null): QueueEntry[]; - listInProgress(): QueueLeaseEntry[]; - markDone(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - markFailed(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - reclaimStuckItem(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - requeueItem(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; - batchClaim(selectFn: (entries: QueueEntry[]) => Array<{ - repoFullName: string; - identifier: string; - apiBaseUrl?: string; - }>): QueueEntry[]; - getAttemptHistory(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueAttemptHistory; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare const QUEUE_STATUSES: readonly QueueStatus[]; -export declare function resolvePortfolioQueueDbPath(env?: Record): string; -/** - * Opens the local portfolio/queue store, creating the table on first use. Rows are ordered highest-priority-first - * with an insertion-order tie-break: `priority DESC, enqueued_at ASC, rowid ASC` — the implicit `rowid` guarantees - * FIFO order even when two items share a priority AND an `enqueued_at` timestamp. (#2292) - */ -export declare function initPortfolioQueueStore(dbPath?: string): PortfolioQueueStore; -export declare function enqueue(item: EnqueueItem): QueueEntry; -export declare function dequeueNext(): QueueEntry | null; -export declare function listQueue(repoFullName?: string | null): QueueEntry[]; -export declare function markDone(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; -export declare function markFailed(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueEntry | null; -export declare function getAttemptHistory(repoFullName: string, identifier: string, apiBaseUrl?: string): QueueAttemptHistory; -export declare function closeDefaultPortfolioQueueStore(): void; diff --git a/packages/loopover-miner/lib/portfolio-queue.js b/packages/loopover-miner/lib/portfolio-queue.js deleted file mode 100644 index c0917054a6..0000000000 --- a/packages/loopover-miner/lib/portfolio-queue.js +++ /dev/null @@ -1,398 +0,0 @@ -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { PORTFOLIO_QUEUE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; -export const QUEUE_STATUSES = Object.freeze(["queued", "in_progress", "done"]); -const defaultDbFileName = "portfolio-queue.sqlite3"; -let defaultPortfolioQueueStore = null; -export function resolvePortfolioQueueDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_PORTFOLIO_QUEUE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolvePortfolioQueueDbPath(), "invalid_portfolio_queue_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const trimmed = repoFullName.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - // #7525: extend #5831's path-safety guard here too — reject a `.`/`..`/control-char segment before it can - // be persisted into SQLite (or echoed back through the CLI), matching claim-ledger.ts's sibling parser. - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeIdentifier(identifier) { - if (typeof identifier !== "string") - throw new Error("invalid_identifier"); - const trimmed = identifier.trim(); - if (!trimmed) - throw new Error("invalid_identifier"); - return trimmed; -} -/** Priority is a placeholder numeric input; an omitted priority defaults to 0, a non-finite or negative one is rejected. */ -function normalizePriority(priority) { - if (priority === undefined || priority === null) - return 0; - if (typeof priority !== "number" || !Number.isFinite(priority) || priority < 0) { - throw new Error("invalid_priority"); - } - return priority; -} -/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563). - * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */ -function normalizeApiBaseUrl(apiBaseUrl) { - if (apiBaseUrl === undefined || apiBaseUrl === null) - return DEFAULT_FORGE_CONFIG.apiBaseUrl; - if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) - throw new Error("invalid_api_base_url"); - return apiBaseUrl.trim(); -} -function rowToEntry(row) { - return { - apiBaseUrl: row.api_base_url, - repoFullName: row.repo_full_name, - identifier: row.identifier, - priority: row.priority, - status: row.status, - enqueuedAt: row.enqueued_at, - }; -} -/** Lease-annotated projection of an in-flight row (adds `leasedAt`), consumed by the expiry sweep. Kept separate - * from `rowToEntry` so the base entry shape every existing caller relies on is unchanged. */ -function rowToLeaseEntry(row) { - return { - apiBaseUrl: row.api_base_url, - repoFullName: row.repo_full_name, - identifier: row.identifier, - status: row.status, - leasedAt: row.leased_at ?? null, - }; -} -function asPortfolioQueueDbRow(row) { - return row; -} -/** - * Opens the local portfolio/queue store, creating the table on first use. Rows are ordered highest-priority-first - * with an insertion-order tie-break: `priority DESC, enqueued_at ASC, rowid ASC` — the implicit `rowid` guarantees - * FIFO order even when two items share a priority AND an `enqueued_at` timestamp. (#2292) - */ -export function initPortfolioQueueStore(dbPath = resolvePortfolioQueueDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - // openLocalStoreDb skips mkdir/chmod for the special in-memory path (':memory:'), which has no file on disk. - const db = openLocalStoreDb(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS miner_portfolio_queue ( - repo_full_name TEXT NOT NULL, - identifier TEXT NOT NULL, - priority REAL NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'in_progress', 'done')), - enqueued_at TEXT NOT NULL, - leased_at TEXT, - PRIMARY KEY (repo_full_name, identifier) - ) - `); - // `leased_at` records when an item was flipped to 'in_progress', so a crashed/killed process's stuck lease can be - // swept back to 'queued' by age (see portfolio-queue-expiry.js) instead of stranding the item forever — the same - // recovery the claim-ledger and worktree-allocator stores already provide for their own tables (#4827). Additive - // migration for stores created before this column: CREATE TABLE IF NOT EXISTS never adds a column to a pre-existing - // table, so add it idempotently. Expressed as the store's first schema migration (#4832): the baseline table is - // version 1; migration 1→2 adds `leased_at`. The migration stays defensive (checks table_info) so a version-0 - // file that already ran the pre-convention ad-hoc ALTER is not re-altered into a duplicate-column error. - // - // v2 -> v3 (#5563): rebuild PRIMARY KEY (repo_full_name, identifier) into PRIMARY KEY (api_base_url, - // repo_full_name, identifier) -- two forge hosts serving a same-named owner/repo must not collide in this - // queue. SQLite cannot ALTER a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy - // every existing row with the pre-#4784 implicit single-forge default backfilled, drop the old table, rename - // the new one in. - applySchemaMigrations(db, [ - (migrationDb) => { - const hasLeasedAtColumn = migrationDb - .prepare("PRAGMA table_info(miner_portfolio_queue)") - .all() - .some((column) => column.name === "leased_at"); - if (!hasLeasedAtColumn) - migrationDb.exec("ALTER TABLE miner_portfolio_queue ADD COLUMN leased_at TEXT"); - }, - (migrationDb) => { - migrationDb.exec(` - CREATE TABLE miner_portfolio_queue_v3 ( - api_base_url TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - identifier TEXT NOT NULL, - priority REAL NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'in_progress', 'done')), - enqueued_at TEXT NOT NULL, - leased_at TEXT, - PRIMARY KEY (api_base_url, repo_full_name, identifier) - ) - `); - // ORDER BY rowid preserves the old table's FIFO insertion order in the new table's freshly-assigned rowids - // (the composite PRIMARY KEY above is not itself the rowid), so this rebuild doesn't reshuffle queue order. - // OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized - // `status`, e.g. from a hand-edited or otherwise corrupted file) would violate the CHECK constraint above - // and abort the whole migration. Skipping it here is consistent with that same fail-closed posture, rather - // than turning one bad row into a permanently unmigratable file. - migrationDb - .prepare(`INSERT OR IGNORE INTO miner_portfolio_queue_v3 - (api_base_url, repo_full_name, identifier, priority, status, enqueued_at, leased_at) - SELECT ?, repo_full_name, identifier, priority, status, enqueued_at, leased_at - FROM miner_portfolio_queue ORDER BY rowid`) - .run(DEFAULT_FORGE_CONFIG.apiBaseUrl); - migrationDb.exec("DROP TABLE miner_portfolio_queue"); - migrationDb.exec("ALTER TABLE miner_portfolio_queue_v3 RENAME TO miner_portfolio_queue"); - }, - // v3 -> v4 (#5654): three attempt-history counters feeding non-convergence.ts's real - // PortfolioConvergenceInput (see getAttemptHistory below) -- additive columns, same - // defensive column-presence guard as the leased_at migration above. - (migrationDb) => { - const existingColumns = migrationDb - .prepare("PRAGMA table_info(miner_portfolio_queue)") - .all() - .map((column) => column.name); - if (!existingColumns.includes("attempts_count")) { - migrationDb.exec("ALTER TABLE miner_portfolio_queue ADD COLUMN attempts_count INTEGER NOT NULL DEFAULT 0"); - } - if (!existingColumns.includes("consecutive_failures")) { - migrationDb.exec("ALTER TABLE miner_portfolio_queue ADD COLUMN consecutive_failures INTEGER NOT NULL DEFAULT 0"); - } - if (!existingColumns.includes("reenqueue_count")) { - migrationDb.exec("ALTER TABLE miner_portfolio_queue ADD COLUMN reenqueue_count INTEGER NOT NULL DEFAULT 0"); - } - }, - // v4 -> v5 (#4939): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of this - // same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing reads - // or writes it yet (no consumer exists until a future hosted deployment populates it). Same defensive - // column-presence guard as the v3->v4 migration immediately above. - (migrationDb) => { - const hasTenantIdColumn = migrationDb - .prepare("PRAGMA table_info(miner_portfolio_queue)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - migrationDb.exec("ALTER TABLE miner_portfolio_queue ADD COLUMN tenant_id TEXT"); - }, - ]); - // `rowid` is a stable, unique key assigned once at first insert (re-enqueue updates in place, never re-inserts), - // so it is a deterministic total-order tie-break: two items sharing a priority AND an `enqueued_at` timestamp - // still order by insertion. - const ORDER = "ORDER BY priority DESC, enqueued_at ASC, rowid ASC"; - // Re-enqueueing an already-tracked item re-activates it IN PLACE: refresh its (placeholder) priority and reset it - // to 'queued', but KEEP the original `enqueued_at` and `rowid` so it holds its existing FIFO position rather than - // jumping the queue. (Restamping `enqueued_at` would be inconsistent — the fixed `rowid` still pins the old - // position whenever timestamps collide — so position is deliberately preserved instead.) - const enqueueStatement = db.prepare(` - INSERT INTO miner_portfolio_queue (api_base_url, repo_full_name, identifier, priority, status, enqueued_at) - VALUES (?, ?, ?, ?, 'queued', ?) - ON CONFLICT(api_base_url, repo_full_name, identifier) DO UPDATE SET - priority = excluded.priority, - status = 'queued' - WHERE miner_portfolio_queue.status <> 'in_progress' - `); - const getStatement = db.prepare("SELECT * FROM miner_portfolio_queue WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ?"); - // Claim the highest-priority queued item ATOMICALLY: one UPDATE selects the ordered top row in a subquery and - // flips it to 'in_progress', RETURNING it — so two processes sharing the file can't both claim the same row (a - // separate SELECT-then-UPDATE would race). Deliberately global (no api_base_url filter): the queue is a single - // cross-host priority ordering, not a per-host one. - // Claiming stamps `leased_at` with the caller-supplied claim time and increments the attempt-history - // `attempts_count` (#5654, non-convergence.ts's real PortfolioConvergenceInput.attempts) -- leaving - // 'in_progress' (done/failed/reclaim) clears leased_at back to NULL so only genuinely in-flight rows carry - // a lease. - const dequeueStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'in_progress', leased_at = ?, attempts_count = attempts_count + 1 - WHERE rowid = ( - SELECT rowid FROM miner_portfolio_queue WHERE status = 'queued' ${ORDER} LIMIT 1 - ) - RETURNING * - `); - // RETURNING (rather than a separate post-UPDATE SELECT) makes the "nothing to mark done" case observable - // directly from one atomic statement. consecutive_failures resets to 0 on reaching done (#5654) -- the - // active failure streak breaks the moment an attempt actually succeeds; reenqueue_count is a lifetime - // total and deliberately untouched here (see getAttemptHistory's own doc comment). - const markDoneStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'done', leased_at = NULL, consecutive_failures = 0 - WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ? AND status <> 'done' - RETURNING * - `); - // Releasing an in-flight item back to queued WITHOUT reaching done is exactly non-convergence.ts's own - // "cycling queued -> in_progress -> queued without ever reaching done" reenqueue trigger (#5654) -- same - // counters, same increment, as reclaimStuckItem below (both are this same transition, just different - // callers: a run-halt release here vs. a stale-lease sweep there). - const markFailedStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'queued', leased_at = NULL, - consecutive_failures = consecutive_failures + 1, reenqueue_count = reenqueue_count + 1 - WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ? AND status = 'in_progress' - RETURNING * - `); - const listAllStatement = db.prepare(`SELECT * FROM miner_portfolio_queue ${ORDER}`); - const listRepoStatement = db.prepare(`SELECT * FROM miner_portfolio_queue WHERE repo_full_name = ? ${ORDER}`); - const listActiveStatement = db.prepare(`SELECT * FROM miner_portfolio_queue WHERE status IN ('queued', 'in_progress') ${ORDER}`); - const listInProgressStatement = db.prepare(`SELECT * FROM miner_portfolio_queue WHERE status = 'in_progress' ${ORDER}`); - // A stale-lease sweep release is the SAME "in_progress -> queued without reaching done" event as - // markFailedStatement above (#5654) -- same counters, same increment. - const reclaimStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'queued', leased_at = NULL, - consecutive_failures = consecutive_failures + 1, reenqueue_count = reenqueue_count + 1 - WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ? AND status = 'in_progress' - RETURNING * - `); - // Requeue only ever targets a COMPLETED ('done') row — an in-flight item is released via reclaimStatement, and - // an already-'queued' item is a no-op — so a caller's manual requeue can never disturb an active claim. The - // row keeps its rowid/enqueued_at, so it re-enters the queue at its original FIFO position, not the back. - // Deliberately leaves attempts_count/consecutive_failures/reenqueue_count untouched (#5654): this is a - // manual reopen of ALREADY-COMPLETED work, not the stuck queued->in_progress->queued cycle those counters - // track -- reachedDone (derived live from status) simply reads false again once requeued, same as any - // other non-done row, until the item is claimed and completed again. - const requeueStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'queued', leased_at = NULL - WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ? AND status = 'done' - RETURNING * - `); - // Same attempts_count increment as dequeueStatement (#5654) -- batchClaim's per-item claim is just as much - // a real attempt as the single-item dequeueNext path. - const claimTargetStatement = db.prepare(` - UPDATE miner_portfolio_queue SET status = 'in_progress', leased_at = ?, attempts_count = attempts_count + 1 - WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ? AND status = 'queued' - RETURNING * - `); - const attemptHistoryStatement = db.prepare("SELECT attempts_count, consecutive_failures, reenqueue_count, status FROM miner_portfolio_queue WHERE api_base_url = ? AND repo_full_name = ? AND identifier = ?"); - return { - dbPath: resolvedPath, - enqueue(item) { - const apiBaseUrl = normalizeApiBaseUrl(item?.apiBaseUrl); - const repoFullName = normalizeRepoFullName(item?.repoFullName); - const identifier = normalizeIdentifier(item?.identifier); - const priority = normalizePriority(item?.priority); - const enqueuedAt = new Date().toISOString(); - enqueueStatement.run(apiBaseUrl, repoFullName, identifier, priority, enqueuedAt); - return rowToEntry(asPortfolioQueueDbRow(getStatement.get(apiBaseUrl, repoFullName, identifier))); - }, - dequeueNext() { - const row = dequeueStatement.get(new Date().toISOString()); - return row ? rowToEntry(asPortfolioQueueDbRow(row)) : null; - }, - /** In-flight ('in_progress') rows with their `leasedAt` claim time, for the expiry sweep (#4827). */ - listInProgress() { - return listInProgressStatement.all().map((row) => rowToLeaseEntry(asPortfolioQueueDbRow(row))); - }, - /** Reclaim a single stuck in-flight item back to 'queued' (clearing its lease), returning it — or null if it is - * no longer 'in_progress' (already finished/reclaimed by another sweep). The sweep target of #4827. */ - reclaimStuckItem(repoFullName, identifier, apiBaseUrl) { - const row = reclaimStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName), normalizeIdentifier(identifier)); - return row ? rowToEntry(asPortfolioQueueDbRow(row)) : null; - }, - /** Requeue a COMPLETED ('done') item back to 'queued' so it is picked up again, keeping its FIFO position - * (rowid/enqueued_at unchanged). Returns the entry, or null when there is no 'done' item to requeue — i.e. - * it is already 'queued', is currently 'in_progress' (release it via {@link reclaimStuckItem} instead), or - * does not exist. The manual counterpart to {@link reclaimStuckItem} for the queue CLI's escape hatch (#4828). */ - requeueItem(repoFullName, identifier, apiBaseUrl) { - const row = requeueStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName), normalizeIdentifier(identifier)); - return row ? rowToEntry(asPortfolioQueueDbRow(row)) : null; - }, - listQueue(repoFullName) { - const rows = repoFullName === undefined || repoFullName === null - ? listAllStatement.all() - : listRepoStatement.all(normalizeRepoFullName(repoFullName)); - return rows.map((row) => rowToEntry(asPortfolioQueueDbRow(row))); - }, - markDone(repoFullName, identifier, apiBaseUrl) { - const row = markDoneStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName), normalizeIdentifier(identifier)); - return row ? rowToEntry(asPortfolioQueueDbRow(row)) : null; - }, - /** Release an in-flight item back to `queued` when a run halts (#2347). */ - markFailed(repoFullName, identifier, apiBaseUrl) { - const row = markFailedStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName), normalizeIdentifier(identifier)); - return row ? rowToEntry(asPortfolioQueueDbRow(row)) : null; - }, - /** - * Transactional caps-aware batch claim hook used by portfolio-queue-manager.js: re-read active rows under an - * exclusive lock, let the caller pick targets, then atomically flip each still-queued row to `in_progress`. - */ - batchClaim(selectFn) { - if (typeof selectFn !== "function") - throw new Error("invalid_batch_claim_selector"); - db.exec("BEGIN IMMEDIATE"); - try { - const entries = listActiveStatement.all().map((row) => rowToEntry(asPortfolioQueueDbRow(row))); - const targets = selectFn(entries); - if (!Array.isArray(targets)) - throw new Error("invalid_batch_claim_selection"); - const leasedAt = new Date().toISOString(); - const claimed = []; - for (const target of targets) { - const apiBaseUrl = normalizeApiBaseUrl(target?.apiBaseUrl); - const repoFullName = normalizeRepoFullName(target?.repoFullName); - const identifier = normalizeIdentifier(target?.identifier); - const row = claimTargetStatement.get(leasedAt, apiBaseUrl, repoFullName, identifier); - if (row) - claimed.push(rowToEntry(asPortfolioQueueDbRow(row))); - } - db.exec("COMMIT"); - return claimed; - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - }, - /** - * A real `PortfolioConvergenceInput` (non-convergence.ts) for one queue item (#5654), replacing the - * first-attempt-shaped literal attempt-input-builder.js previously hardcoded. An item never enqueued here - * (not yet tracked at all) reads the same honest zero-state as a genuine first attempt -- absence of - * history is not evidence of a problem, same rule non-convergence.ts's own header documents. `reachedDone` - * is derived live from the row's current `status`, not a separate persisted flag (see requeueStatement's - * comment above for why that's the deliberate choice). - */ - getAttemptHistory(repoFullName, identifier, apiBaseUrl) { - const row = attemptHistoryStatement.get(normalizeApiBaseUrl(apiBaseUrl), normalizeRepoFullName(repoFullName), normalizeIdentifier(identifier)); - if (!row) - return { attempts: 0, consecutiveFailures: 0, reenqueues: 0, reachedDone: false }; - const historyRow = asPortfolioQueueDbRow(row); - return { - attempts: historyRow.attempts_count, - consecutiveFailures: historyRow.consecutive_failures, - reenqueues: historyRow.reenqueue_count, - reachedDone: historyRow.status === "done", - }; - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically. - purgeByRepo(repoFullName) { - return purgeStoreByRepo(db, PORTFOLIO_QUEUE_PURGE_SPEC, normalizeRepoFullName(repoFullName)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultPortfolioQueueStore() { - defaultPortfolioQueueStore ??= initPortfolioQueueStore(); - return defaultPortfolioQueueStore; -} -export function enqueue(item) { - return getDefaultPortfolioQueueStore().enqueue(item); -} -export function dequeueNext() { - return getDefaultPortfolioQueueStore().dequeueNext(); -} -export function listQueue(repoFullName) { - return getDefaultPortfolioQueueStore().listQueue(repoFullName); -} -export function markDone(repoFullName, identifier, apiBaseUrl) { - return getDefaultPortfolioQueueStore().markDone(repoFullName, identifier, apiBaseUrl); -} -export function markFailed(repoFullName, identifier, apiBaseUrl) { - return getDefaultPortfolioQueueStore().markFailed(repoFullName, identifier, apiBaseUrl); -} -export function getAttemptHistory(repoFullName, identifier, apiBaseUrl) { - return getDefaultPortfolioQueueStore().getAttemptHistory(repoFullName, identifier, apiBaseUrl); -} -export function closeDefaultPortfolioQueueStore() { - if (!defaultPortfolioQueueStore) - return; - defaultPortfolioQueueStore.close(); - defaultPortfolioQueueStore = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydGZvbGlvLXF1ZXVlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicG9ydGZvbGlvLXF1ZXVlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3pELE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxnQkFBZ0IsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3hHLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3JELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzVELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBa0Z0RixNQUFNLENBQUMsTUFBTSxjQUFjLEdBQTJCLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFRLEVBQUUsYUFBYSxFQUFFLE1BQU0sQ0FBVSxDQUFDLENBQUM7QUFFaEgsTUFBTSxpQkFBaUIsR0FBRyx5QkFBeUIsQ0FBQztBQUNwRCxJQUFJLDBCQUEwQixHQUErQixJQUFJLENBQUM7QUFFbEUsTUFBTSxVQUFVLDJCQUEyQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQy9GLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsbUNBQW1DLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsMkJBQTJCLEVBQUUsRUFBRSxpQ0FBaUMsQ0FBQyxDQUFDO0FBQzdHLENBQUM7QUFFRCxTQUFTLHFCQUFxQixDQUFDLFlBQXFCO0lBQ2xELElBQUksT0FBTyxZQUFZLEtBQUssUUFBUTtRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUNoRixNQUFNLE9BQU8sR0FBRyxZQUFZLENBQUMsSUFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/pr-disposition-poller.d.ts b/packages/loopover-miner/lib/pr-disposition-poller.d.ts deleted file mode 100644 index d96aa515fd..0000000000 --- a/packages/loopover-miner/lib/pr-disposition-poller.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -export type PrDisposition = { - state: "open" | "closed"; - merged: boolean; - closedAt: string | null; - attempts: number; -}; -export type PollPrDispositionOptions = { - apiBaseUrl?: string; - fetchFn?: typeof fetch; - githubToken?: string; - maxAttempts?: number; - minIntervalMs?: number; - maxIntervalMs?: number; - requestTimeoutMs?: number; - sleepFn?: (delayMs: number) => Promise; -}; -/** - * Poll a real PR's own merge/close disposition (distinct from its CI check-run conclusion, ci-poller.js's - * concern) with exponential backoff, until it reaches a terminal `state: "closed"` or `maxAttempts` is - * exhausted -- whichever comes first. A still-`"open"` PR after the last attempt is returned as-is, not an - * error: an unattended loop cycle should treat "still open" as "not yet resolved", not fail. - * - * @param {string} repoFullName - * @param {number} prNumber - * @param {{ - * apiBaseUrl?: string, fetchFn?: typeof fetch, githubToken?: string, maxAttempts?: number, - * minIntervalMs?: number, maxIntervalMs?: number, sleepFn?: (delayMs: number) => Promise, - * }} [options] - * @returns {Promise<{ state: "open"|"closed", merged: boolean, closedAt: string|null, attempts: number }>} - */ -export declare function pollPrDisposition(repoFullName: string, prNumber: number, options?: PollPrDispositionOptions): Promise; -/** - * Classify a real, terminal PR disposition into loop-reentry.js's own `candidate.outcome` vocabulary - * (`"merged"|"disengaged"|"other"`). A still-open disposition (not yet resolved) classifies as `"other"` -- - * the same bucket a runMinerAttempt outcome that never opened a PR at all falls into (nothing to re-enter on - * yet, in either case). - * - * @param {{ state: "open"|"closed", merged: boolean }} disposition - * @returns {"merged"|"disengaged"|"other"} - */ -export declare function classifyPrDisposition(disposition: Pick): "merged" | "disengaged" | "other"; diff --git a/packages/loopover-miner/lib/pr-disposition-poller.js b/packages/loopover-miner/lib/pr-disposition-poller.js deleted file mode 100644 index 40e61472d3..0000000000 --- a/packages/loopover-miner/lib/pr-disposition-poller.js +++ /dev/null @@ -1,164 +0,0 @@ -// Real PR-disposition poller (#5135, Wave 3.5 -- the autonomous loop). ci-poller.js already polls a PR's CI -// check-runs, but that answers a DIFFERENT question ("did the checks pass") from what the supervising loop -// needs at cycle-close time ("did the PR itself get merged or closed"). Nothing in this package answered that -// second question before this file: pr-outcome.js already has a real store for the classification -// (recordPrOutcomeSnapshot/readPrOutcomes), but every existing caller of it was a test -- this is the real -// GitHub fetch that produces the classification pr-outcome.js's writer expects. -// -// Deliberately its own module, not folded into ci-poller.js: the two pollers ask genuinely different -// questions (check-run conclusion vs. PR merge/close disposition) with different terminal conditions (a -// check-run poll's "pending" means "wait for the SAME head commit's checks to finish"; a disposition poll's -// "open" means "wait for a human to actually merge or close the PR", a potentially much longer, unbounded -// wait) -- composing them into one poller would conflate two different backoff/timeout policies. -import { fetchWithRetry } from "./http-retry.js"; -const defaultApiBaseUrl = "https://api.github.com"; -const defaultMinIntervalMs = 60_000; -const defaultMaxIntervalMs = 5 * 60_000; -const defaultMaxAttempts = 1; -const defaultRequestTimeoutMs = 10_000; -const githubApiVersion = "2022-11-28"; -function normalizeApiBaseUrl(value) { - if (value === undefined) - return defaultApiBaseUrl; - if (typeof value !== "string" || !value.trim()) - return defaultApiBaseUrl; - let parsed; - try { - parsed = new URL(value.trim()); - } - catch { - throw new Error("invalid_api_base_url"); - } - if (parsed.protocol !== "https:" || parsed.hostname !== "api.github.com") { - throw new Error("invalid_api_base_url"); - } - parsed.pathname = parsed.pathname.replace(/\/+$/, ""); - parsed.search = ""; - parsed.hash = ""; - return parsed.toString().replace(/\/+$/, ""); -} -function normalizePositiveInt(value, fallback, min, max) { - if (!Number.isFinite(value)) - return fallback; - return Math.min(max, Math.max(min, Math.floor(value))); -} -function normalizeOptions(options = {}) { - return { - apiBaseUrl: normalizeApiBaseUrl(options.apiBaseUrl), - fetchFn: options.fetchFn ?? fetch, - githubToken: typeof options.githubToken === "string" ? options.githubToken.trim() : "", - maxAttempts: normalizePositiveInt(options.maxAttempts, defaultMaxAttempts, 1, 20), - minIntervalMs: normalizePositiveInt(options.minIntervalMs, defaultMinIntervalMs, 1, 60 * 60_000), - maxIntervalMs: normalizePositiveInt(options.maxIntervalMs, defaultMaxIntervalMs, 1, 60 * 60_000), - requestTimeoutMs: normalizePositiveInt(options.requestTimeoutMs, defaultRequestTimeoutMs, 1, 60_000), - sleepFn: options.sleepFn ?? - ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))), - }; -} -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner?.trim() || !repo?.trim() || extra !== undefined) { - throw new Error("invalid_repo_full_name"); - } - return { owner: owner.trim(), repo: repo.trim() }; -} -function normalizePullNumber(value) { - if (!Number.isInteger(value) || value <= 0) - throw new Error("invalid_pr_number"); - return value; -} -function githubHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "user-agent": "loopover-miner", - "x-github-api-version": githubApiVersion, - }; - if (githubToken) - headers.authorization = `Bearer ${githubToken}`; - return headers; -} -function repoPath(target, suffix) { - return `/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}${suffix}`; -} -function apiUrl(apiBaseUrl, path) { - return `${apiBaseUrl}${path}`; -} -function githubError(response, payload) { - const code = `github_${response.status}`; - const githubMessage = typeof payload?.message === "string" && payload.message.trim() ? payload.message : null; - const message = githubMessage ? `${code}: ${githubMessage}` : code; - return Object.assign(new Error(message), { code, githubMessage }); -} -async function fetchPullRequest(target, prNumber, options) { - // Retry transient network errors / 5xx around this single call (#4829), matching ci-poller.js's - // githubGetJsonResponse -- distinct from this poller's OWN outer pending-retry loop. requestTimeoutMs bounds - // each individual attempt with a fresh AbortSignal.timeout() (a stalled connection can't hang a poll cycle - // forever -- #miner-github-read-timeouts); the injected sleepFn keeps the retry backoff instant in tests. - const response = await fetchWithRetry(options.fetchFn, apiUrl(options.apiBaseUrl, repoPath(target, `/pulls/${prNumber}`)), { method: "GET", headers: githubHeaders(options.githubToken) }, { sleepFn: options.sleepFn, timeoutMs: options.requestTimeoutMs }); - const payload = (await response.json().catch(() => null)); - if (!response.ok) - throw githubError(response, payload); - return payload; -} -/** GitHub's own vocabulary is `state: "open"|"closed"` plus a separate `merged: boolean` -- "closed and not - * merged" is the disengaged case. A still-open PR is never terminal for this poller's purposes. */ -function normalizeDisposition(payload) { - const state = payload?.state === "closed" ? "closed" : "open"; - const merged = Boolean(payload?.merged); - const closedAt = typeof payload?.closed_at === "string" ? payload.closed_at : null; - return { state, merged, closedAt }; -} -function backoffDelayMs(attemptIndex, options) { - const exponent = Math.min(10, Math.max(0, attemptIndex)); - return Math.min(options.maxIntervalMs, options.minIntervalMs * 2 ** exponent); -} -/** - * Poll a real PR's own merge/close disposition (distinct from its CI check-run conclusion, ci-poller.js's - * concern) with exponential backoff, until it reaches a terminal `state: "closed"` or `maxAttempts` is - * exhausted -- whichever comes first. A still-`"open"` PR after the last attempt is returned as-is, not an - * error: an unattended loop cycle should treat "still open" as "not yet resolved", not fail. - * - * @param {string} repoFullName - * @param {number} prNumber - * @param {{ - * apiBaseUrl?: string, fetchFn?: typeof fetch, githubToken?: string, maxAttempts?: number, - * minIntervalMs?: number, maxIntervalMs?: number, sleepFn?: (delayMs: number) => Promise, - * }} [options] - * @returns {Promise<{ state: "open"|"closed", merged: boolean, closedAt: string|null, attempts: number }>} - */ -export async function pollPrDisposition(repoFullName, prNumber, options = {}) { - const target = parseRepoFullName(repoFullName); - const normalizedPrNumber = normalizePullNumber(prNumber); - const normalizedOptions = normalizeOptions(options); - let latest = { state: "open", merged: false, closedAt: null, attempts: 0 }; - for (let attempt = 0; attempt < normalizedOptions.maxAttempts; attempt += 1) { - const payload = await fetchPullRequest(target, normalizedPrNumber, normalizedOptions); - latest = { ...normalizeDisposition(payload), attempts: attempt + 1 }; - if (latest.state === "closed") - return latest; - if (attempt === normalizedOptions.maxAttempts - 1) - return latest; - await normalizedOptions.sleepFn(backoffDelayMs(attempt, normalizedOptions)); - } - // Unreachable at runtime: maxAttempts is normalized to >= 1, so the loop always returns on its final iteration - // (the `attempt === maxAttempts - 1` guard). Kept only to satisfy the compiler's all-paths-return requirement. - /* v8 ignore next -- unreachable: the normalized maxAttempts >= 1 loop always returns before falling through */ - return latest; -} -/** - * Classify a real, terminal PR disposition into loop-reentry.js's own `candidate.outcome` vocabulary - * (`"merged"|"disengaged"|"other"`). A still-open disposition (not yet resolved) classifies as `"other"` -- - * the same bucket a runMinerAttempt outcome that never opened a PR at all falls into (nothing to re-enter on - * yet, in either case). - * - * @param {{ state: "open"|"closed", merged: boolean }} disposition - * @returns {"merged"|"disengaged"|"other"} - */ -export function classifyPrDisposition(disposition) { - if (disposition.state !== "closed") - return "other"; - return disposition.merged ? "merged" : "disengaged"; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHItZGlzcG9zaXRpb24tcG9sbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHItZGlzcG9zaXRpb24tcG9sbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDRHQUE0RztBQUM1RywyR0FBMkc7QUFDM0csOEdBQThHO0FBQzlHLGtHQUFrRztBQUNsRywyR0FBMkc7QUFDM0csZ0ZBQWdGO0FBQ2hGLEVBQUU7QUFDRixxR0FBcUc7QUFDckcsd0dBQXdHO0FBQ3hHLDRHQUE0RztBQUM1RywwR0FBMEc7QUFDMUcsaUdBQWlHO0FBRWpHLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQTBDakQsTUFBTSxpQkFBaUIsR0FBRyx3QkFBd0IsQ0FBQztBQUNuRCxNQUFNLG9CQUFvQixHQUFHLE1BQU0sQ0FBQztBQUNwQyxNQUFNLG9CQUFvQixHQUFHLENBQUMsR0FBRyxNQUFNLENBQUM7QUFDeEMsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLENBQUM7QUFDN0IsTUFBTSx1QkFBdUIsR0FBRyxNQUFNLENBQUM7QUFDdkMsTUFBTSxnQkFBZ0IsR0FBRyxZQUFZLENBQUM7QUFFdEMsU0FBUyxtQkFBbUIsQ0FBQyxLQUFjO0lBQ3pDLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxPQUFPLGlCQUFpQixDQUFDO0lBQ2xELElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRTtRQUFFLE9BQU8saUJBQWlCLENBQUM7SUFDekUsSUFBSSxNQUFNLENBQUM7SUFDWCxJQUFJLENBQUM7UUFDSCxNQUFNLEdBQUcsSUFBSSxHQUFHLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7SUFDakMsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE1BQU0sSUFBSSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMxQyxDQUFDO0lBQ0QsSUFBSSxNQUFNLENBQUMsUUFBUSxLQUFLLFFBQVEs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/pr-number-parse.d.ts b/packages/loopover-miner/lib/pr-number-parse.d.ts deleted file mode 100644 index 1b61d71231..0000000000 --- a/packages/loopover-miner/lib/pr-number-parse.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** `gh pr create` (local-write-tools.ts's `buildOpenPrSpec` -- no `--json` flag) prints the created PR's own - * URL to stdout on success; this is `gh`'s real, documented, stable CLI behavior, not an invented contract. - * Scoped to the exact target repo so an unrelated URL elsewhere in stdout/stderr noise can never match. - */ -export declare function parsePrNumberFromExecResult(execResult: { - stdout?: string | undefined; - code?: number | null | undefined; - timedOut?: boolean | undefined; -} | null | undefined, repoFullName: string): number | null; diff --git a/packages/loopover-miner/lib/pr-number-parse.js b/packages/loopover-miner/lib/pr-number-parse.js deleted file mode 100644 index 72ba9d760a..0000000000 --- a/packages/loopover-miner/lib/pr-number-parse.js +++ /dev/null @@ -1,20 +0,0 @@ -// Shared PR-number extraction from a real `gh pr create` executeLocalWrite result (#4848). `gh pr create` -// prints the new PR's URL to stdout on success -- this is the one place that URL is authoritatively parsed, -// so loop-cli.js's CI/gate-status polling and attempt-cli.js's post-submission claim-conflict check agree on -// exactly how a PR number is recovered from a real command's raw output. -/** `gh pr create` (local-write-tools.ts's `buildOpenPrSpec` -- no `--json` flag) prints the created PR's own - * URL to stdout on success; this is `gh`'s real, documented, stable CLI behavior, not an invented contract. - * Scoped to the exact target repo so an unrelated URL elsewhere in stdout/stderr noise can never match. - */ -export function parsePrNumberFromExecResult(execResult, repoFullName) { - if (!execResult || execResult.timedOut || execResult.code !== 0 || typeof execResult.stdout !== "string") { - return null; - } - const escapedRepo = repoFullName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const match = execResult.stdout.match(new RegExp(`github\\.com/${escapedRepo}/pull/(\\d+)`)); - if (!match) - return null; - const prNumber = Number(match[1]); - return Number.isInteger(prNumber) && prNumber > 0 ? prNumber : null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHItbnVtYmVyLXBhcnNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHItbnVtYmVyLXBhcnNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDBHQUEwRztBQUMxRyw0R0FBNEc7QUFDNUcsNkdBQTZHO0FBQzdHLHlFQUF5RTtBQUV6RTs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsMkJBQTJCLENBQ3pDLFVBQWdJLEVBQ2hJLFlBQW9CO0lBRXBCLElBQUksQ0FBQyxVQUFVLElBQUksVUFBVSxDQUFDLFFBQVEsSUFBSSxVQUFVLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxPQUFPLFVBQVUsQ0FBQyxNQUFNLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDekcsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQ0QsTUFBTSxXQUFXLEdBQUcsWUFBWSxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsRUFBRSxNQUFNLENBQUMsQ0FBQztJQUN4RSxNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxJQUFJLE1BQU0sQ0FBQyxnQkFBZ0IsV0FBVyxjQUFjLENBQUMsQ0FBQyxDQUFDO0lBQzdGLElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDeEIsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2xDLE9BQU8sTUFBTSxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsSUFBSSxRQUFRLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztBQUN0RSxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/pr-outcome.d.ts b/packages/loopover-miner/lib/pr-outcome.d.ts deleted file mode 100644 index 81e7d723bd..0000000000 --- a/packages/loopover-miner/lib/pr-outcome.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { AppendEventInput, LedgerEntry } from "./event-ledger.js"; -/** Event-ledger vocabulary for a miner-local PR outcome. */ -export declare const MINER_PR_OUTCOME_EVENT: "pr_outcome"; -/** The terminal decisions a miner records for one of its own PRs. */ -export declare const MINER_PR_OUTCOME_DECISIONS: readonly ["merged", "closed"]; -export type MinerPrOutcomeDecision = (typeof MINER_PR_OUTCOME_DECISIONS)[number]; -export type NormalizedPrOutcomePayload = { - prNumber: number; - decision: MinerPrOutcomeDecision; - closedAt: string | null; - reason: string | null; -}; -export type PrOutcomeInput = { - repoFullName?: unknown; - prNumber?: unknown; - decision?: unknown; - closedAt?: unknown; - reason?: unknown; -}; -export type RecordPrOutcomeOptions = { - /** Optional at the type level so a caller can pass an unusable ledger to exercise the fail-closed guard; the - * writer throws `invalid_event_ledger` at runtime when this is absent or lacks `appendEvent`. Reuses the - * real EventLedger#appendEvent signature so a genuine EventLedger (not just a same-shaped stub) type-checks. */ - eventLedger?: { - appendEvent(event: AppendEventInput): LedgerEntry; - }; -}; -export type PrOutcomeLedgerReader = { - readEvents(filter?: { - since?: number; - repoFullName?: string; - }): unknown[]; -}; -/** - * Validate + normalize a PR-outcome payload; returns `null` on any malformed shape (mirrors manage-status.js's - * `normalizeManageUpdatePayload`, so a bad row can neither be written nor read back). A `closed` decision may carry - * a reason bucket drawn from {@link REJECTION_REASONS} (shared with the rejection-state-machine sibling); a `merged` - * decision — or an unrecognized reason — normalizes the reason to `null` (a merged PR has no rejection reason). - */ -export declare function normalizePrOutcomePayload(payload: unknown): NormalizedPrOutcomePayload | null; -/** - * Thin writer over an INJECTED event ledger (same dependency-injection shape as manage-poll.js's - * `recordManagePollSnapshot`, so it's unit-testable without a real ledger file). Appends one - * {@link MINER_PR_OUTCOME_EVENT} scoped to the repo and returns the appended entry. Fail-soft on a malformed - * snapshot: a missing repo or an invalid payload returns `null` rather than throwing (an unusable ledger is the - * only hard error, since that is a programmer wiring mistake). - */ -export declare function recordPrOutcomeSnapshot(input: PrOutcomeInput, options?: RecordPrOutcomeOptions): unknown; -/** - * Reconstruct the latest outcome per repo/PR from the ledger's ascending append-only event stream (mirrors - * manage-status.js's `indexLatestManageUpdates`). Reads via the injected ledger's `readEvents(filter)` and reduces - * the pure result — a later event for the same repo/PR supersedes an earlier one. Returns a `Map` keyed by - * `repoFullName:prNumber`. - */ -export declare function readPrOutcomes(eventLedger: PrOutcomeLedgerReader | null | undefined, filter?: { - since?: number; - repoFullName?: string; -}): Map; diff --git a/packages/loopover-miner/lib/pr-outcome.js b/packages/loopover-miner/lib/pr-outcome.js deleted file mode 100644 index a4ec93d12f..0000000000 --- a/packages/loopover-miner/lib/pr-outcome.js +++ /dev/null @@ -1,105 +0,0 @@ -// Miner-local PR-outcome record (#4274). The miner's OWN local record of the outcomes of its OWN PRs — merged or -// closed — written to the miner's local SQLite via the generic append-only event-ledger.js, mirroring how -// manage-status.js layers a specific typed event (MANAGE_PR_UPDATE_EVENT + a payload normalizer + a thin writer) -// on top of that same ledger. -// -// DISTINCT from the server-side `pr_outcome` concept: src/review/outcomes-wire.ts's `recordPrOutcome` writes -// `pr_outcome` rows to the HOSTED backend's D1 audit tables from the GitHub App's webhook stream — that is the -// loopover SERVER recording ground truth for every contributor. THIS is a laptop-mode miner's local record of -// its own PRs (it may have no webhook relay at all): same concept name, different codebase layer, no shared code. -// The distinct `MINER_PR_OUTCOME_EVENT` local constant keeps the two from being conflated. -import { REJECTION_REASONS } from "./rejection-templates.js"; -/** Event-ledger vocabulary for a miner-local PR outcome. */ -export const MINER_PR_OUTCOME_EVENT = "pr_outcome"; -/** The terminal decisions a miner records for one of its own PRs. */ -export const MINER_PR_OUTCOME_DECISIONS = Object.freeze(["merged", "closed"]); -const decisionSet = new Set(MINER_PR_OUTCOME_DECISIONS); -const reasonSet = new Set(REJECTION_REASONS); -function optionalString(value) { - if (value === undefined || value === null) - return null; - if (typeof value !== "string") - return null; - const trimmed = value.trim(); - return trimmed || null; -} -/** - * Validate + normalize a PR-outcome payload; returns `null` on any malformed shape (mirrors manage-status.js's - * `normalizeManageUpdatePayload`, so a bad row can neither be written nor read back). A `closed` decision may carry - * a reason bucket drawn from {@link REJECTION_REASONS} (shared with the rejection-state-machine sibling); a `merged` - * decision — or an unrecognized reason — normalizes the reason to `null` (a merged PR has no rejection reason). - */ -export function normalizePrOutcomePayload(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return null; - const record = payload; - if (!Number.isInteger(record.prNumber) || record.prNumber <= 0) - return null; - const decision = optionalString(record.decision); - if (!decision || !decisionSet.has(decision)) - return null; - const reasonRaw = optionalString(record.reason); - const reason = decision === "closed" && reasonRaw !== null && reasonSet.has(reasonRaw) ? reasonRaw : null; - return { - prNumber: record.prNumber, - decision: decision, - closedAt: optionalString(record.closedAt), - reason, - }; -} -/** - * Thin writer over an INJECTED event ledger (same dependency-injection shape as manage-poll.js's - * `recordManagePollSnapshot`, so it's unit-testable without a real ledger file). Appends one - * {@link MINER_PR_OUTCOME_EVENT} scoped to the repo and returns the appended entry. Fail-soft on a malformed - * snapshot: a missing repo or an invalid payload returns `null` rather than throwing (an unusable ledger is the - * only hard error, since that is a programmer wiring mistake). - */ -export function recordPrOutcomeSnapshot(input, options = {}) { - const eventLedger = options.eventLedger; - if (!eventLedger || typeof eventLedger.appendEvent !== "function") - throw new Error("invalid_event_ledger"); - const repoFullName = typeof input.repoFullName === "string" ? input.repoFullName.trim() : ""; - if (!repoFullName) - return null; - const payload = normalizePrOutcomePayload({ - prNumber: input.prNumber, - decision: input.decision, - closedAt: input.closedAt, - reason: input.reason, - }); - if (!payload) - return null; - return eventLedger.appendEvent({ type: MINER_PR_OUTCOME_EVENT, repoFullName, payload }); -} -/** - * Reconstruct the latest outcome per repo/PR from the ledger's ascending append-only event stream (mirrors - * manage-status.js's `indexLatestManageUpdates`). Reads via the injected ledger's `readEvents(filter)` and reduces - * the pure result — a later event for the same repo/PR supersedes an earlier one. Returns a `Map` keyed by - * `repoFullName:prNumber`. - */ -export function readPrOutcomes(eventLedger, filter = {}) { - const events = eventLedger && typeof eventLedger.readEvents === "function" ? eventLedger.readEvents(filter) : []; - const latest = new Map(); - for (const event of Array.isArray(events) ? events : []) { - if (!event || typeof event !== "object") - continue; - const row = event; - if (row.type !== MINER_PR_OUTCOME_EVENT) - continue; - if (typeof row.repoFullName !== "string" || !row.repoFullName.trim()) - continue; - const normalized = normalizePrOutcomePayload(row.payload); - if (!normalized) - continue; - // Re-key on every event so Map iteration order tracks most-recently-UPDATED last, not first-seen (#7222). A - // bare Map.set() on an existing key updates the value but leaves the key frozen at its original position, so a - // later outcome for the same PR (e.g. closed-without-merge, then reopened + merged) stayed at its old slot -- - // breaking recency-ordered consumers like loop-reentry.js's countConsecutiveDisengagements. Deleting first - // moves the freshly-updated entry to the end, matching this reducer's own "a later event supersedes" contract. - const key = `${row.repoFullName}:${normalized.prNumber}`; - latest.delete(key); - latest.set(key, { ...normalized, repoFullName: row.repoFullName }); - } - return latest; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHItb3V0Y29tZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInByLW91dGNvbWUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsaUhBQWlIO0FBQ2pILDBHQUEwRztBQUMxRyxpSEFBaUg7QUFDakgsOEJBQThCO0FBQzlCLEVBQUU7QUFDRiw2R0FBNkc7QUFDN0csK0dBQStHO0FBQy9HLDhHQUE4RztBQUM5RyxrSEFBa0g7QUFDbEgsMkZBQTJGO0FBRTNGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBRzdELDREQUE0RDtBQUM1RCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxZQUFxQixDQUFDO0FBRTVELHFFQUFxRTtBQUNyRSxNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUSxFQUFFLFFBQVEsQ0FBVSxDQUFDLENBQUM7QUE4QnZGLE1BQU0sV0FBVyxHQUFHLElBQUksR0FBRyxDQUFTLDBCQUEwQixDQUFDLENBQUM7QUFDaEUsTUFBTSxTQUFTLEdBQUcsSUFBSSxHQUFHLENBQVMsaUJBQWlCLENBQUMsQ0FBQztBQUVyRCxTQUFTLGNBQWMsQ0FBQyxLQUFjO0lBQ3BDLElBQUksS0FBSyxLQUFLLFNBQVMsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3ZELElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQzNDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM3QixPQUFPLE9BQU8sSUFBSSxJQUFJLENBQUM7QUFDekIsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLHlCQUF5QixDQUFDLE9BQWdCO0lBQ3hELElBQUksQ0FBQyxPQUFPLElBQUksT0FBTyxPQUFPLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDO1FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/prediction-ledger.d.ts b/packages/loopover-miner/lib/prediction-ledger.d.ts deleted file mode 100644 index 39db201346..0000000000 --- a/packages/loopover-miner/lib/prediction-ledger.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type PredictionLedgerEntry = { - id: number; - ts: string; - repoFullName: string; - targetId: number; - headSha: string | null; - conclusion: string; - pack: string; - readinessScore: number | null; - blockerCodes: string[]; - warningCodes: string[]; - engineVersion: string; -}; -export type AppendPredictionInput = { - repoFullName: string; - targetId: number; - headSha?: string | null; - conclusion: string; - pack: string; - readinessScore?: number | null; - blockerCodes?: string[]; - warningCodes?: string[]; - engineVersion: string; -}; -export type ReadPredictionsFilter = { - repoFullName?: string | null; -}; -export type PredictionLedger = { - dbPath: string; - appendPrediction(input: AppendPredictionInput): PredictionLedgerEntry; - readPredictions(filter?: ReadPredictionsFilter): PredictionLedgerEntry[]; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare function resolvePredictionLedgerDbPath(env?: Record): string; -/** - * Opens the append-only prediction ledger, creating the table on first use. Rows are returned in ascending `id` - * order (insertion order). (#4263) - */ -export declare function initPredictionLedger(dbPath?: string): PredictionLedger; -export declare function appendPrediction(input: AppendPredictionInput): PredictionLedgerEntry; -export declare function readPredictions(filter?: ReadPredictionsFilter): PredictionLedgerEntry[]; -export declare function closeDefaultPredictionLedger(): void; diff --git a/packages/loopover-miner/lib/prediction-ledger.js b/packages/loopover-miner/lib/prediction-ledger.js deleted file mode 100644 index 2d0e2c64c4..0000000000 --- a/packages/loopover-miner/lib/prediction-ledger.js +++ /dev/null @@ -1,210 +0,0 @@ -import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { PREDICTION_LEDGER_PURGE_SPEC, PREDICTION_LEDGER_RETENTION_SPEC, purgeStoreByRepo, pruneLedgerByRetention, resolveLedgerRetentionPolicy, } from "./store-maintenance.js"; -const defaultDbFileName = "prediction-ledger.sqlite3"; -let defaultPredictionLedger = null; -export function resolvePredictionLedgerDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_PREDICTION_LEDGER_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolvePredictionLedgerDbPath(), "invalid_prediction_ledger_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeOptionalRepoFullName(repoFullName) { - if (repoFullName === undefined || repoFullName === null) - return undefined; - return normalizeRepoFullName(repoFullName); -} -function requiredNonEmptyString(value, error) { - if (typeof value !== "string" || !value.trim()) - throw new Error(error); - return value.trim(); -} -function optionalString(value) { - if (value === undefined || value === null) - return null; - if (typeof value !== "string") - throw new Error("invalid_head_sha"); - const trimmed = value.trim(); - return trimmed || null; -} -// Codes are stored as a JSON array of the non-empty trimmed strings, in order — a stable, small projection of a -// verdict's blockers/warnings that drops all free-text detail. -function normalizeCodes(codes, error) { - if (codes === undefined || codes === null) - return []; - if (!Array.isArray(codes)) - throw new Error(error); - return codes.map((code) => { - if (typeof code !== "string" || !code.trim()) - throw new Error(error); - return code.trim(); - }); -} -function normalizeReadinessScore(value) { - if (value === undefined || value === null) - return null; - if (typeof value !== "number" || !Number.isFinite(value)) - throw new Error("invalid_readiness_score"); - return value; -} -/** Validate + normalize an append input, throwing on any invalid field (mirrors normalizeGovernorLedgerEvent). */ -function normalizePredictionInput(input) { - if (!input || typeof input !== "object" || Array.isArray(input)) - throw new Error("invalid_prediction_input"); - if (!Number.isInteger(input.targetId) || input.targetId <= 0) - throw new Error("invalid_target_id"); - return { - repoFullName: normalizeRepoFullName(input.repoFullName), - targetId: input.targetId, - headSha: optionalString(input.headSha), - conclusion: requiredNonEmptyString(input.conclusion, "invalid_conclusion"), - pack: requiredNonEmptyString(input.pack, "invalid_pack"), - readinessScore: normalizeReadinessScore(input.readinessScore), - blockerCodes: normalizeCodes(input.blockerCodes, "invalid_blocker_codes"), - warningCodes: normalizeCodes(input.warningCodes, "invalid_warning_codes"), - engineVersion: requiredNonEmptyString(input.engineVersion, "invalid_engine_version"), - }; -} -function rowToEntry(row) { - let blockerCodes; - let warningCodes; - try { - blockerCodes = JSON.parse(row.blocker_codes_json); - warningCodes = JSON.parse(row.warning_codes_json); - if (!Array.isArray(blockerCodes) || !Array.isArray(warningCodes)) - throw new Error("corrupted_prediction_row"); - } - catch { - throw new Error("corrupted_prediction_row"); - } - return { - id: row.id, - ts: row.ts, - repoFullName: row.repo_full_name, - targetId: row.target_id, - headSha: row.head_sha, - conclusion: row.conclusion, - pack: row.pack, - readinessScore: row.readiness_score, - blockerCodes: blockerCodes, - warningCodes: warningCodes, - engineVersion: row.engine_version, - }; -} -function asPredictionDbRow(row) { - return row; -} -// v1 -> v2 (#4939): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of this -// same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing reads or -// writes it yet (no consumer exists until a future hosted deployment populates it). Same defensive -// column-presence guard as this file's sibling stores' own additive migrations (e.g. event-ledger.js's and -// run-state.js's own tenant_id additions), so re-running it against an already-migrated file is a no-op. -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(predictions)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE predictions ADD COLUMN tenant_id TEXT"); -} -/** - * Opens the append-only prediction ledger, creating the table on first use. Rows are returned in ascending `id` - * order (insertion order). (#4263) - */ -export function initPredictionLedger(dbPath = resolvePredictionLedgerDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - // Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): the INSERT/SELECT CRUD goes through - // `driver.query`, while schema creation/migrations, retention pruning, and the repo-scoped purge still use the - // underlying DatabaseSync until those helpers are migrated. Public API stays synchronous (part-1 slice). - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS predictions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ts TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - target_id INTEGER NOT NULL, - head_sha TEXT, - conclusion TEXT NOT NULL, - pack TEXT NOT NULL, - readiness_score REAL, - blocker_codes_json TEXT NOT NULL, - warning_codes_json TEXT NOT NULL, - engine_version TEXT NOT NULL - ) - `); - db.exec("CREATE INDEX IF NOT EXISTS idx_predictions_repo ON predictions (repo_full_name, id)"); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addTenantIdColumn]); - // Opt-in retention (#4834): prune aged/excess rows when an operator has enabled it; a no-op by default. - pruneLedgerByRetention(db, PREDICTION_LEDGER_RETENTION_SPEC, resolveLedgerRetentionPolicy(), Date.now()); - const appendSql = ` - INSERT INTO predictions - (ts, repo_full_name, target_id, head_sha, conclusion, pack, readiness_score, blocker_codes_json, warning_codes_json, engine_version) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - const getByIdSql = "SELECT * FROM predictions WHERE id = ?"; - const readAllSql = "SELECT * FROM predictions ORDER BY id ASC"; - const readByRepoSql = "SELECT * FROM predictions WHERE repo_full_name = ? ORDER BY id ASC"; - return { - dbPath: resolvedPath, - appendPrediction(input) { - const n = normalizePredictionInput(input); - const ts = new Date().toISOString(); - // A plain INSERT (no RETURNING) is a zero-result-column statement, so `driver.query` runs it on the write - // path and returns the coerced `lastInsertRowid` to re-read the row just written. - const { lastInsertRowid } = driver.query(appendSql, [ - ts, - n.repoFullName, - n.targetId, - n.headSha, - n.conclusion, - n.pack, - n.readinessScore, - JSON.stringify(n.blockerCodes), - JSON.stringify(n.warningCodes), - n.engineVersion, - ]); - return rowToEntry(asPredictionDbRow(driver.query(getByIdSql, [lastInsertRowid]).rows[0])); - }, - readPredictions(filter = {}) { - const repoFullName = normalizeOptionalRepoFullName(filter.repoFullName); - const rows = repoFullName === undefined - ? driver.query(readAllSql, []).rows - : driver.query(readByRepoSql, [repoFullName]).rows; - return rows.map((row) => rowToEntry(asPredictionDbRow(row))); - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. See the - // IMMUTABILITY INVARIANT note above: this is a deliberate, separate exception, not a normal ledger write. - purgeByRepo(repoFullName) { - return purgeStoreByRepo(db, PREDICTION_LEDGER_PURGE_SPEC, normalizeRepoFullName(repoFullName)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultPredictionLedger() { - defaultPredictionLedger ??= initPredictionLedger(); - return defaultPredictionLedger; -} -export function appendPrediction(input) { - return getDefaultPredictionLedger().appendPrediction(input); -} -export function readPredictions(filter) { - return getDefaultPredictionLedger().readPredictions(filter); -} -export function closeDefaultPredictionLedger() { - if (!defaultPredictionLedger) - return; - defaultPredictionLedger.close(); - defaultPredictionLedger = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlZGljdGlvbi1sZWRnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwcmVkaWN0aW9uLWxlZGdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUseUJBQXlCLEVBQUUscUJBQXFCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM3RyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUM1RCxPQUFPLEVBQ0wsNEJBQTRCLEVBQzVCLGdDQUFnQyxFQUNoQyxnQkFBZ0IsRUFDaEIsc0JBQXNCLEVBQ3RCLDRCQUE0QixHQUM3QixNQUFNLHdCQUF3QixDQUFDO0FBaUVoQyxNQUFNLGlCQUFpQixHQUFHLDJCQUEyQixDQUFDO0FBQ3RELElBQUksdUJBQXVCLEdBQTRCLElBQUksQ0FBQztBQUU1RCxNQUFNLFVBQVUsNkJBQTZCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDakcsT0FBTyx1QkFBdUIsQ0FBQyxpQkFBaUIsRUFBRSxxQ0FBcUMsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUNoRyxDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsTUFBYztJQUNyQyxPQUFPLHlCQUF5QixDQUFDLE1BQU0sRUFBRSw2QkFBNkIsRUFBRSxFQUFFLG1DQUFtQyxDQUFDLENBQUM7QUFDakgsQ0FBQztBQUVELFNBQVMscUJBQXFCLENBQUMsWUFBb0I7SUFDakQsSUFBSSxPQUFPLFlBQVksS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ2hGLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDNUQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUztRQUFFLE1BQU0sSUFBSSxLQUFL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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/pretooluse-hook.d.ts b/packages/loopover-miner/lib/pretooluse-hook.d.ts deleted file mode 100644 index c2a91c4eb4..0000000000 --- a/packages/loopover-miner/lib/pretooluse-hook.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type DenyRule } from "./deny-hooks.js"; -import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js"; -export type BuildHouseRulesPreToolUseHookConfig = { - rules?: readonly DenyRule[]; - repoFullName?: string; -}; -export type BuildHouseRulesPreToolUseHookOptions = { - append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry; -}; -/** Minimal shape this module reads from the real Agent SDK `PreToolUseHookInput`. */ -export type PreToolUseHookLikeInput = { - tool_name?: string; - tool_input?: Record; - hook_event_name?: string; -}; -export type PreToolUseHookJSONOutput = { - hookSpecificOutput?: { - hookEventName: "PreToolUse"; - permissionDecision: "deny"; - permissionDecisionReason: string; - }; -}; -/** - * Build a Claude Agent SDK `PreToolUse` hook callback enforcing the house-rule denylist. Register the - * returned function under `options.hooks.PreToolUse` (e.g. `{ hooks: [PreToolUse: [{ hooks: [built] }]] }` - * on the object passed to `createAgentSdkCodingAgentDriver({ hooks })`). - * - * House rules are sourced from a single, auditable list: {@link DEFAULT_DENY_RULES} by default, or an - * effective rule set built by the caller (e.g. `resolveEffectiveDenyRules` from deny-hook-synthesis.js, - * merging in maintainer-approved synthesized rules) — this module composes whatever rule set it is given, - * it does not own deriving one. - */ -export declare function buildHouseRulesPreToolUseHook(config?: BuildHouseRulesPreToolUseHookConfig, options?: BuildHouseRulesPreToolUseHookOptions): (input: PreToolUseHookLikeInput, toolUseId?: string, context?: unknown) => Promise>; diff --git a/packages/loopover-miner/lib/pretooluse-hook.js b/packages/loopover-miner/lib/pretooluse-hook.js deleted file mode 100644 index 8143d2a62a..0000000000 --- a/packages/loopover-miner/lib/pretooluse-hook.js +++ /dev/null @@ -1,84 +0,0 @@ -// PreToolUse-hook-enforced house rules (#2343). Wraps the pure `evaluateDenyHooks` decision function -// (deny-hooks.js, #2295) into a real Claude Agent SDK PreToolUse hook callback -- the actual live -// interception point a CodingAgentDriver session registers via `options.hooks.PreToolUse` (the exact -// seam `agent-sdk-driver.ts`'s `hooks` passthrough documents as "#2343's stated attachment point"). -// -// WHY THIS HOLDS EVEN UNDER bypassPermissions: per the Agent SDK's own documented permission-evaluation -// order (https://code.claude.com/docs/en/agent-sdk/permissions), hooks run FIRST -- before deny rules, -// ask rules, the permission mode check, and allow rules -- and "Hooks still execute and can block -// operations if needed" even when `permissionMode: 'bypassPermissions'` is set: "Deny rules -// (disallowed_tools), explicit ask rules, and hooks are evaluated before the mode check and can still -// block a tool." This module does not implement that guarantee -- the SDK does. This module's job is -// only to return a correctly-shaped, fail-closed deny decision every time; the SDK is what makes that -// decision unbypassable. -// -// FAIL CLOSED: any internal error (a malformed tool-call shape, a governor-ledger append failure) denies -// rather than silently allowing. -import { DEFAULT_DENY_RULES, evaluateDenyHooks } from "./deny-hooks.js"; -import { appendGovernorEvent } from "./governor-ledger.js"; -function recordDenial(append, repoFullName, reason, payload) { - try { - append({ - eventType: "denied", - repoFullName: repoFullName ?? null, - actionClass: "pretooluse_hook", - decision: "deny", - reason, - payload, - }); - } - catch { - // A ledger append failure must never suppress or alter the deny decision itself -- the tool call is - // still blocked even if the audit write fails. Silently allowing on a logging failure would be a far - // worse outcome for a security boundary than an unrecorded (but still enforced) denial. - } -} -function denyOutput(reason) { - return { - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: reason, - }, - }; -} -/** - * Build a Claude Agent SDK `PreToolUse` hook callback enforcing the house-rule denylist. Register the - * returned function under `options.hooks.PreToolUse` (e.g. `{ hooks: [PreToolUse: [{ hooks: [built] }]] }` - * on the object passed to `createAgentSdkCodingAgentDriver({ hooks })`). - * - * House rules are sourced from a single, auditable list: {@link DEFAULT_DENY_RULES} by default, or an - * effective rule set built by the caller (e.g. `resolveEffectiveDenyRules` from deny-hook-synthesis.js, - * merging in maintainer-approved synthesized rules) — this module composes whatever rule set it is given, - * it does not own deriving one. - */ -export function buildHouseRulesPreToolUseHook(config = {}, options = {}) { - const rules = config.rules ?? DEFAULT_DENY_RULES; - const repoFullName = config.repoFullName; - const append = options.append ?? appendGovernorEvent; - return async function houseRulesPreToolUseHook(input) { - try { - const toolName = input && typeof input === "object" ? input.tool_name : undefined; - const toolInput = input && typeof input === "object" ? input.tool_input : undefined; - const verdict = evaluateDenyHooks({ name: toolName, input: toolInput }, [...rules]); - if (verdict.allowed) - return {}; - // `verdict.blockedBy` is always set together with `!verdict.allowed` (evaluateDenyHooks's only two return - // shapes), and `.matcher` is always a defined string on it (ruleMatches gates every match on - // `typeof rule.matcher === "string"` -- a rule can never become `blockedBy` otherwise). `.reason` has no - // equivalent gate, so a caller-supplied custom rule omitting it is a real, reachable case. - const reason = verdict.blockedBy.reason ?? "House rule denylist match."; - recordDenial(append, repoFullName, reason, { - toolName: typeof toolName === "string" ? toolName : null, - matcher: verdict.blockedBy.matcher, - }); - return denyOutput(reason); - } - catch (error) { - const reason = `pretooluse_hook_internal_error: ${error instanceof Error ? error.message : String(error)}`; - recordDenial(append, repoFullName, reason, {}); - return denyOutput(reason); - } - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJldG9vbHVzZS1ob29rLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHJldG9vbHVzZS1ob29rLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHFHQUFxRztBQUNyRyxrR0FBa0c7QUFDbEcscUdBQXFHO0FBQ3JHLG9HQUFvRztBQUNwRyxFQUFFO0FBQ0Ysd0dBQXdHO0FBQ3hHLHVHQUF1RztBQUN2RyxrR0FBa0c7QUFDbEcsNEZBQTRGO0FBQzVGLHNHQUFzRztBQUN0RyxxR0FBcUc7QUFDckcsc0dBQXNHO0FBQ3RHLHlCQUF5QjtBQUN6QixFQUFFO0FBQ0YseUdBQXlHO0FBQ3pHLGlDQUFpQztBQUVqQyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsaUJBQWlCLEVBQWlCLE1BQU0saUJBQWlCLENBQUM7QUFDdkYsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUEyQjNELFNBQVMsWUFBWSxDQUNuQixNQUFnRSxFQUNoRSxZQUFnQyxFQUNoQyxNQUFjLEVBQ2QsT0FBZ0M7SUFFaEMsSUFBSSxDQUFDO1FBQ0gsTUFBTSxDQUFDO1lBQ0wsU0FBUyxFQUFFLFFBQVE7WUFDbkIsWUFBWSxFQUFFLFlBQVksSUFBSSxJQUFJO1lBQ2xDLFdBQVcsRUFBRSxpQkFBaUI7WUFDOUIsUUFBUSxFQUFFLE1BQU07WUFDaEIsTUFBTTtZQUNOLE9BQU87U0FDUixDQUFDLENBQUM7SUFDTCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1Asb0dBQW9HO1FBQ3BHLHFHQUFxRztRQUNyRyx3RkFBd0Y7SUFDMUYsQ0FBQztBQUNILENBQUM7QUFFRCxTQUFTLFVBQVUsQ0FBQyxNQUFjO0lBQ2hDLE9BQU87UUFDTCxrQkFBa0IsRUFBRTtZQUNsQixhQUFhLEVBQUUsWUFBWTtZQUMzQixrQkFBa0IsRUFBRSxNQUFNO1lBQzFCLHdCQUF3QixFQUFFLE1BQU07U0FDakM7S0FDRixDQUFDO0FBQ0osQ0FBQztBQUVEOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sVUFBVSw2QkFBNkIsQ0FDM0MsU0FBOEMsRUFBRSxFQUNoRCxVQUFnRCxFQUFFO0lBTWxELE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxLQUFLLElBQUksa0JBQWtCLENBQUM7SUFDakQsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQztJQUN6QyxNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxJQUFJLG1CQUFtQixDQUFDO0lBRXJELE9BQU8sS0FBSyxVQUFVLHdCQUF3QixDQUFDLEtBQUs7UUFDbEQsSUFBSSxDQUFDO1lBQ0gsTUFBTSxRQUFRLEdBQUcsS0FBSyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1lBQ2xGLE1BQU0sU0FBUyxHQUFHLEtBQUssSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztZQUNwRixNQUFNLE9BQU8sR0FBRyxpQkFBaUIsQ0FDL0IsRUFBRSxJQUFJLEVBQUUsUUFBa0IsRUFBRSxLQUFLLEVBQUUsU0FBb0MsRUFBRSxFQUN6RSxDQUFDLEdBQUcsS0FBSyxDQUFDLENBQ1gsQ0FBQztZQUVGLElBQUksT0FBTyxDQUFDLE9BQU87Z0JBQUUsT0FBTyxFQUFFLENBQUM7WUFFL0IsMEdBQTBHO1lBQzFHLDZGQUE2RjtZQUM3Rix5R0FBeUc7WUFDekcsMkZBQTJGO1lBQzNGLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxTQUFVLENBQUMsTUFBTSxJQUFJLDRCQUE0QixDQUFDO1lBQ3pFLFlBQVksQ0FBQyxNQUFNLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRTtnQkFDekMsUUFBUSxFQUFFLE9BQU8sUUFBUSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJO2dCQUN4RCxPQUFPLEVBQUUsT0FBTyxDQUFDLFNBQVUsQ0FBQyxPQUFPO2FBQ3BDLENBQUMsQ0FBQztZQUNILE9BQU8sVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzVCLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsTUFBTSxNQUFNLEdBQUcsbUNBQW1DLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1lBQzNHLFlBQVksQ0FBQyxNQUFNLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQztZQUMvQyxPQUFPLFVBQVUsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUM1QixDQUFDO0lBQ0gsQ0FBQyxDQUFDO0FBQ0osQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/process-lifecycle.d.ts b/packages/loopover-miner/lib/process-lifecycle.d.ts deleted file mode 100644 index eb0849b57f..0000000000 --- a/packages/loopover-miner/lib/process-lifecycle.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** Process lifecycle / crash-safety for the miner CLI (#4826). The CLI dispatches through a chain of bare - * `process.exit()` calls with no cleanup hook, so a SIGINT/SIGTERM mid-run — or an uncaught exception — used to - * kill the process mid-write, leaving whatever local SQLite ledger it was touching in an undefined state. This - * module is the single cleanup chokepoint: local stores register themselves when opened (see `local-store.js`), and - * `installCliSignalHandlers` (called once at CLI startup) flushes/closes every still-open resource before exiting - * cleanly on a signal, and logs + exits non-zero on an uncaught exception / unhandled rejection instead of crashing - * silently. Cleanup ONLY — no command business logic lives here. Every dependency (`process`, `log`, `exit`) is - * injectable so the handlers are unit-testable without actually signalling the test runner. */ -/** A closable store (`{ close() }`) or a plain cleanup callback. */ -export type CleanupResource = { - close: () => void; -} | (() => void); -/** The subset of `process` the handlers use; injectable for tests. */ -export type ProcessLike = { - on: (event: string, listener: (...args: unknown[]) => void) => unknown; - exit: (code?: number) => void; -}; -export type InstallCliSignalHandlersOptions = { - process?: ProcessLike; - log?: (message: string) => void; - exit?: (code: number) => void; - /** Called (in addition to `log`) for uncaughtException/unhandledRejection specifically -- not the clean - * SIGINT/SIGTERM exits, which are not errors. AWAITED before the process exits, so it should both capture - * AND flush (see captureMinerErrorAndFlush in bin/loopover-miner.js) -- a synchronous capture alone only - * queues the event, which process.exit() would then likely never deliver. No-op default. Never expected to - * throw/reject. */ - captureError?: (error: unknown, context?: Record) => void | Promise; - /** Reinstall even if handlers were already installed (mainly for tests). */ - force?: boolean; -}; -/** - * Register a resource to be closed on clean exit or crash. Returns an idempotent unregister function (call it from - * the resource's own normal `close()` so a resource closed during the happy path is not double-closed at exit). - */ -export declare function registerCleanupResource(resource: CleanupResource | null | undefined): () => void; -/** Number of currently-registered cleanup resources (exposed for tests / diagnostics). */ -export declare function cleanupResourceCount(): number; -/** - * Close every registered resource, swallowing each individual failure (a store that fails to close must not stop - * the others from closing) and reporting it via `options.onError`. Idempotent: the registry is emptied afterwards. - */ -export declare function closeAllCleanupResources(options?: { - onError?: (error: unknown) => void; -}): void; -/** - * Install top-level signal + error handlers once. On SIGINT/SIGTERM: close all resources and exit with the - * conventional 128+signal code. On uncaughtException/unhandledRejection: log the error, AWAIT the optional - * captureError hook (so a captured Sentry event has a chance to actually flush before the process exits), - * close all resources, and exit non-zero. No-op (returns false) if already installed unless `options.force` is - * set. All of `process`, `log`, `exit`, and `captureError` are injectable for testing. - */ -export declare function installCliSignalHandlers(options?: InstallCliSignalHandlersOptions): boolean; -/** Test-only: clear the registry and the installed flag so each test starts from a clean lifecycle. */ -export declare function resetProcessLifecycleForTesting(): void; diff --git a/packages/loopover-miner/lib/process-lifecycle.js b/packages/loopover-miner/lib/process-lifecycle.js deleted file mode 100644 index 2189df7871..0000000000 --- a/packages/loopover-miner/lib/process-lifecycle.js +++ /dev/null @@ -1,114 +0,0 @@ -/** Process lifecycle / crash-safety for the miner CLI (#4826). The CLI dispatches through a chain of bare - * `process.exit()` calls with no cleanup hook, so a SIGINT/SIGTERM mid-run — or an uncaught exception — used to - * kill the process mid-write, leaving whatever local SQLite ledger it was touching in an undefined state. This - * module is the single cleanup chokepoint: local stores register themselves when opened (see `local-store.js`), and - * `installCliSignalHandlers` (called once at CLI startup) flushes/closes every still-open resource before exiting - * cleanly on a signal, and logs + exits non-zero on an uncaught exception / unhandled rejection instead of crashing - * silently. Cleanup ONLY — no command business logic lives here. Every dependency (`process`, `log`, `exit`) is - * injectable so the handlers are unit-testable without actually signalling the test runner. */ -// 128 + signal number, the conventional shell exit code for a process terminated by that signal (SIGINT=2 -> 130, -// SIGTERM=15 -> 143). -const SIGNAL_EXIT_CODES = Object.freeze({ SIGINT: 130, SIGTERM: 143 }); -/** Resources to close on exit. A resource is either a `{ close() }` object (e.g. an open SQLite store) or a plain - * cleanup function. Held in insertion order so cleanup is deterministic. */ -const cleanupResources = new Set(); -let handlersInstalled = false; -/** Render any thrown value as a single log-safe string, preferring an Error's stack. */ -function describeError(value) { - if (value instanceof Error) - return value.stack ?? value.message; - return String(value); -} -/** - * Register a resource to be closed on clean exit or crash. Returns an idempotent unregister function (call it from - * the resource's own normal `close()` so a resource closed during the happy path is not double-closed at exit). - */ -export function registerCleanupResource(resource) { - if (resource === null || resource === undefined) - return () => { }; - cleanupResources.add(resource); - return () => { - cleanupResources.delete(resource); - }; -} -/** Number of currently-registered cleanup resources (exposed for tests / diagnostics). */ -export function cleanupResourceCount() { - return cleanupResources.size; -} -/** - * Close every registered resource, swallowing each individual failure (a store that fails to close must not stop - * the others from closing) and reporting it via `options.onError`. Idempotent: the registry is emptied afterwards. - */ -export function closeAllCleanupResources(options = {}) { - const onError = typeof options.onError === "function" ? options.onError : null; - for (const resource of [...cleanupResources]) { - try { - if (typeof resource === "function") - resource(); - else - resource.close(); - } - catch (error) { - if (onError) - onError(error); - } - } - cleanupResources.clear(); -} -/** - * Install top-level signal + error handlers once. On SIGINT/SIGTERM: close all resources and exit with the - * conventional 128+signal code. On uncaughtException/unhandledRejection: log the error, AWAIT the optional - * captureError hook (so a captured Sentry event has a chance to actually flush before the process exits), - * close all resources, and exit non-zero. No-op (returns false) if already installed unless `options.force` is - * set. All of `process`, `log`, `exit`, and `captureError` are injectable for testing. - */ -export function installCliSignalHandlers(options = {}) { - const proc = options.process ?? process; - const log = typeof options.log === "function" ? options.log : (message) => console.error(message); - const exit = typeof options.exit === "function" ? options.exit : (code) => proc.exit(code); - // Optional Sentry (or any) capture hook -- decoupled from a specific implementation so this module stays - // fully unit-testable without mocking Sentry (#6011). No-op default matches this module's pre-existing - // behavior for every caller that doesn't pass one. - const captureError = typeof options.captureError === "function" ? options.captureError : () => { }; - if (handlersInstalled && options.force !== true) - return false; - handlersInstalled = true; - const runCleanup = () => { - closeAllCleanupResources({ - onError: (error) => log(`loopover-miner: cleanup error while exiting: ${describeError(error)}`), - }); - }; - for (const [signal, code] of Object.entries(SIGNAL_EXIT_CODES)) { - proc.on(signal, () => { - log(`loopover-miner: received ${signal}, closing open resources and exiting.`); - runCleanup(); - exit(code); - }); - } - // Awaited (not fire-and-forget): captureError is expected to both capture AND flush before returning (see - // captureMinerErrorAndFlush in bin/loopover-miner.js) -- Sentry.captureException only QUEUES an event, and - // process.exit() tears the process down immediately without waiting for any pending HTTP delivery, so a - // synchronous capture-then-exit would make the crash-capture path a near-total no-op in practice. Node does - // not require these handlers to be synchronous: nothing exits the process until this handler itself calls - // `exit()`, so awaiting first is safe. captureError's own default is a synchronous no-op, so `await`-ing it - // is a harmless no-op for every caller that doesn't pass one. - proc.on("uncaughtException", async (error) => { - log(`loopover-miner: uncaught exception: ${describeError(error)}`); - await captureError(error, { kind: "uncaughtException" }); - runCleanup(); - exit(1); - }); - proc.on("unhandledRejection", async (reason) => { - log(`loopover-miner: unhandled promise rejection: ${describeError(reason)}`); - await captureError(reason, { kind: "unhandledRejection" }); - runCleanup(); - exit(1); - }); - return true; -} -/** Test-only: clear the registry and the installed flag so each test starts from a clean lifecycle. */ -export function resetProcessLifecycleForTesting() { - cleanupResources.clear(); - handlersInstalled = false; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1saWZlY3ljbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwcm9jZXNzLWxpZmVjeWNsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7OzsrRkFPK0Y7QUF5Qi9GLGtIQUFrSDtBQUNsSCxzQkFBc0I7QUFDdEIsTUFBTSxpQkFBaUIsR0FBMkIsTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsT0FBTyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUM7QUFFL0Y7NEVBQzRFO0FBQzVFLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxHQUFHLEVBQW1CLENBQUM7QUFDcEQsSUFBSSxpQkFBaUIsR0FBRyxLQUFLLENBQUM7QUFFOUIsd0ZBQXdGO0FBQ3hGLFNBQVMsYUFBYSxDQUFDLEtBQWM7SUFDbkMsSUFBSSxLQUFLLFlBQVksS0FBSztRQUFFLE9BQU8sS0FBSyxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDO0lBQ2hFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3ZCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsdUJBQXVCLENBQUMsUUFBNEM7SUFDbEYsSUFBSSxRQUFRLEtBQUssSUFBSSxJQUFJLFFBQVEsS0FBSyxTQUFTO1FBQUUsT0FBTyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUM7SUFDakUsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQy9CLE9BQU8sR0FBRyxFQUFFO1FBQ1YsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3BDLENBQUMsQ0FBQztBQUNKLENBQUM7QUFFRCwwRkFBMEY7QUFDMUYsTUFBTSxVQUFVLG9CQUFvQjtJQUNsQyxPQUFPLGdCQUFnQixDQUFDLElBQUksQ0FBQztBQUMvQixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLHdCQUF3QixDQUFDLFVBQWtELEVBQUU7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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/prompt-injection-defense.d.ts b/packages/loopover-miner/lib/prompt-injection-defense.d.ts deleted file mode 100644 index f26df5b8aa..0000000000 --- a/packages/loopover-miner/lib/prompt-injection-defense.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export declare const PROMPT_INJECTION_RE: RegExp; -/** True when the text contains an agent-manipulation / prompt-injection pattern. */ -export declare function hasPromptInjection(text: string | null | undefined): boolean; -/** - * Replace injection-like spans with a defanged marker so the literal manipulation never reaches the - * coding agent verbatim. Returns the neutralized text + whether anything was flagged. - */ -export declare function neutralizePromptInjection(text: string | null | undefined): { - text: string; - injected: boolean; -}; diff --git a/packages/loopover-miner/lib/prompt-injection-defense.js b/packages/loopover-miner/lib/prompt-injection-defense.js deleted file mode 100644 index 8302365199..0000000000 --- a/packages/loopover-miner/lib/prompt-injection-defense.js +++ /dev/null @@ -1,43 +0,0 @@ -// Detect + defang prompt-injection / agent-manipulation text in UNTRUSTED third-party repo content (a -// customer's own issue title/body) before it reaches the coding agent's own instructions (#4795). Such -// content is DATA, never instructions -- but a coding agent operating with real write authority on a -// customer's repository can still be steered by it, so we both flag it (a strong negative signal) and -// redact the literal manipulation so it can't be obeyed verbatim. -// -// SELF-CONTAINED NATIVE PORT: byte-faithful to src/review/prompt-injection.ts's proven regex (the same -// reviewer-manipulation shape, now defending the coding agent's own instructions instead of the AI -// reviewer's prompt). No cross-package import -- packages/loopover-miner never depends on root src/ (a -// separate Cloudflare Worker deployable, see package.json's own dependency list), so the pattern is -// duplicated here rather than shared, matching how src/review/prompt-injection.ts itself documents being -// a self-contained port of its own upstream (reviewbot's src/core/prompt-injection.ts). Keep the two -// regex sources in sync by hand if either evolves. -const INJECTION_SOURCE = [ - "\\b(?:ignore|disregard|forget)\\b[^.]{0,40}\\b(?:previous|prior|above|earlier|all|the|any)\\b[^.]{0,24}\\b(?:instructions?|prompts?|rules?|rubric|policy|guidelines?|directions?)\\b", - "\\b(?:override|bypass)\\b[^.]{0,40}\\b(?:previous|prior|above|earlier|all|any)\\b[^.]{0,24}\\b(?:instructions?|prompts?)\\b|\\b(?:override|bypass)\\s+the\\s+(?:rules?|rubric|policy|guidelines?|directions?)\\b[^.]{0,40}\\b(?:approve|merge|accept|whitelist|allow|pass|scor(?:e|ing))\\b", - "\\byou are now\\s+(?:an?\\s+(?:\\w+\\s+)?(?:ai|assistant|language model|reviewer|maintainer|admin|moderator|bot|developer|owner|system)|(?:unrestricted|uncensored|unfiltered|unbound|jailbroken))\\b", - "\\b(?:this is|here is|below is)\\s+the\\s+(?:system|developer)\\s+prompt\\b|\\b(?:system|developer)\\s+prompt\\s*:", - "\\b(?:approve|merge|accept|whitelist|allow|pass)\\s+this\\s+(?:submission|pr|pull[ -]?request|entry|request|content|review)\\b|\\b(?:please|kindly|just)\\s+(?:approve|merge|accept|whitelist|allow|pass)\\s+the\\s+(?:submission|pr|pull[ -]?request|entry|request|content|review)\\b", - "\\bas an?\\s+(?:ai|assistant|language model)\\b[^.]{0,30}\\b(?:you must\\s+(?:ignore|approve|obey|disregard|comply)|ignore\\s+(?:previous|prior|all|the|any)|approve\\s+(?:this|the))\\b", - "\\b(?:print|reveal|output|repeat|leak)\\b[^.]{0,30}\\byour\\s+(?:system prompt|rubric|instructions?)\\b|\\b(?:print|reveal|output|repeat|leak)\\b[^.]{0,30}\\bthe\\s+(?:system|developer)\\s+prompt\\b[^.]{0,40}\\byou\\s+(?:were\\s+)?(?:given|sent|provided|received)\\b", - "\\b(?:pretend|roleplay)\\b[^.]{0,24}\\byou\\s+are\\b", -].join("|"); -export const PROMPT_INJECTION_RE = new RegExp(INJECTION_SOURCE, "i"); -/** True when the text contains an agent-manipulation / prompt-injection pattern. */ -export function hasPromptInjection(text) { - return !!text && PROMPT_INJECTION_RE.test(text); -} -/** - * Replace injection-like spans with a defanged marker so the literal manipulation never reaches the - * coding agent verbatim. Returns the neutralized text + whether anything was flagged. - */ -export function neutralizePromptInjection(text) { - if (!text) - return { text: text ?? "", injected: false }; - let injected = false; - const cleaned = text.replace(new RegExp(INJECTION_SOURCE, "gi"), () => { - injected = true; - return "[external-instruction-redacted]"; - }); - return { text: cleaned, injected }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvbXB0LWluamVjdGlvbi1kZWZlbnNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHJvbXB0LWluamVjdGlvbi1kZWZlbnNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHNHQUFzRztBQUN0Ryx1R0FBdUc7QUFDdkcscUdBQXFHO0FBQ3JHLHNHQUFzRztBQUN0RyxrRUFBa0U7QUFDbEUsRUFBRTtBQUNGLHVHQUF1RztBQUN2RyxtR0FBbUc7QUFDbkcsdUdBQXVHO0FBQ3ZHLG9HQUFvRztBQUNwRyx5R0FBeUc7QUFDekcscUdBQXFHO0FBQ3JHLG1EQUFtRDtBQUVuRCxNQUFNLGdCQUFnQixHQUFHO0lBQ3ZCLHNMQUFzTDtJQUN0TCw2UkFBNlI7SUFDN1IsdU1BQXVNO0lBQ3ZNLG9IQUFvSDtJQUNwSCx3UkFBd1I7SUFDeFIsMExBQTBMO0lBQzFMLDRRQUE0UTtJQUM1USxzREFBc0Q7Q0FDdkQsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7QUFFWixNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBRyxJQUFJLE1BQU0sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUVyRSxvRkFBb0Y7QUFDcEYsTUFBTSxVQUFVLGtCQUFrQixDQUFDLElBQStCO0lBQ2hFLE9BQU8sQ0FBQyxDQUFDLElBQUksSUFBSSxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDbEQsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSx5QkFBeUIsQ0FBQyxJQUErQjtJQUN2RSxJQUFJLENBQUMsSUFBSTtRQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxJQUFJLEVBQUUsRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDeEQsSUFBSSxRQUFRLEdBQUcsS0FBSyxDQUFDO0lBQ3JCLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxNQUFNLENBQUMsZ0JBQWdCLEVBQUUsSUFBSSxDQUFDLEVBQUUsR0FBRyxFQUFFO1FBQ3BFLFFBQVEsR0FBRyxJQUFJLENBQUM7UUFDaEIsT0FBTyxpQ0FBaUMsQ0FBQztJQUMzQyxDQUFDLENBQUMsQ0FBQztJQUNILE9BQU8sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxDQUFDO0FBQ3JDLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/purge-cli.d.ts b/packages/loopover-miner/lib/purge-cli.d.ts deleted file mode 100644 index 1514c1342a..0000000000 --- a/packages/loopover-miner/lib/purge-cli.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { ClaimLedger } from "./claim-ledger.js"; -import type { EventLedger } from "./event-ledger.js"; -import type { GovernorLedger } from "./governor-ledger.js"; -import type { PredictionLedger } from "./prediction-ledger.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -import type { RunStateStore } from "./run-state.js"; -import type { ContributionProfileCache } from "./contribution-profile-cache.js"; -import type { GovernorState } from "./governor-state.js"; -import type { PolicyVerdictCacheStore } from "./policy-verdict-cache.js"; -export declare const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; -export type PurgeCliOptions = { - openClaimLedger?: () => ClaimLedger; - initEventLedger?: () => EventLedger; - initGovernorLedger?: () => GovernorLedger; - initPredictionLedger?: () => PredictionLedger; - initPortfolioQueueStore?: () => PortfolioQueueStore; - initRunStateStore?: () => RunStateStore; - initContributionProfileCache?: () => ContributionProfileCache; - openGovernorState?: () => GovernorState; - initPolicyVerdictCacheStore?: () => PolicyVerdictCacheStore; - resolveDbPaths?: Record string>; -}; -export type ParsedPurgeArgs = { - json: boolean; - dryRun: boolean; - repoFullName: string; -} | { - error: string; -}; -export declare function parsePurgeArgs(args: string[]): ParsedPurgeArgs; -export type PurgeDryRunStoreResult = { - store: string; - wouldPurge: number | null; - error?: string; -}; -export type PurgeDryRunResult = { - outcome: "dry_run"; - repoFullName: string; - stores: PurgeDryRunStoreResult[]; - attemptLogNote: string; - attemptLogTotalRows: number; -}; -export declare function runPurgeDryRun(parsed: { - repoFullName: string; - json: boolean; -}, options?: PurgeCliOptions): number; -export type PurgeStoreResult = { - store: string; - purged: number | null; - error?: string; - note?: string; -}; -export type PurgeSummary = { - outcome: "purged" | "partial"; - repoFullName: string; - totalPurged: number; - stores: PurgeStoreResult[]; - purgedAt: string; -}; -export declare function runPurge(args: string[], options?: PurgeCliOptions): number; diff --git a/packages/loopover-miner/lib/purge-cli.js b/packages/loopover-miner/lib/purge-cli.js deleted file mode 100644 index ab8858de5f..0000000000 --- a/packages/loopover-miner/lib/purge-cli.js +++ /dev/null @@ -1,204 +0,0 @@ -// `loopover-miner purge` (#5564, #6599): an explicit, operator-invoked right-to-be-forgotten path across the local -// ledgers. Deletes every row for one repo from the stores that have a real `repoColumn` (claim-ledger, -// event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state, contribution-profile-cache, and -// governor-state's two repo-scoped tables — #7091), via each store's own `purgeByRepo` method (which reuses -// `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`). -// `attempt-log.js` is deliberately reported as not-purgeable rather than silently skipped or approximated: its -// payload is a free-form `Record` with no dedicated repo column, so a precise per-repo match -// isn't possible there without risking false matches -- see store-maintenance.js's own purge-spec doc comment. -// -// Every purge is audit-observable by design (#5564's own acceptance criteria): the real (non-dry-run) path -// always prints a per-store summary, even under --json, so a purge can never be silent. A failure in one store -// does not prevent reporting what succeeded in the others -- see purgeOneStore's own per-store try/catch. -import { existsSync } from "node:fs"; -import { DatabaseSync } from "node:sqlite"; -import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js"; -import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js"; -import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js"; -import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js"; -import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; -import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js"; -import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; -import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; -import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; -import { resolveAttemptLogDbPath } from "./attempt-log.js"; -import { CLAIM_LEDGER_PURGE_SPEC, EVENT_LEDGER_PURGE_SPEC, GOVERNOR_LEDGER_PURGE_SPEC, PREDICTION_LEDGER_PURGE_SPEC, PORTFOLIO_QUEUE_PURGE_SPEC, RUN_STATE_PURGE_SPEC, CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, POLICY_VERDICT_CACHE_PURGE_SPEC, countStoreByRepo, describeError, } from "./store-maintenance.js"; -import { argsWantJson, reportCliFailure } from "./cli-error.js"; -const PURGE_USAGE = "Usage: loopover-miner purge --repo [--dry-run] [--json]"; -export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; -const REAL_PURGE_TARGETS = [ - { name: "claim-ledger", optionKey: "openClaimLedger", opener: openClaimLedger, resolveDbPath: resolveClaimLedgerDbPath, spec: CLAIM_LEDGER_PURGE_SPEC }, - { name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC }, - { name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC }, - { name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC }, - { name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC }, - { name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC }, - { name: "contribution-profile-cache", optionKey: "initContributionProfileCache", opener: initContributionProfileCache, resolveDbPath: resolveContributionProfileCacheDbPath, spec: CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC }, - // governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a - // single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091). - { name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] }, - { name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC }, -]; -function parseRepoArg(value, usage) { - if (!value) - return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -export function parsePurgeArgs(args) { - const options = { - json: false, - dryRun: false, - repoFullName: null, - }; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - // Only the flag-look-alike case is checked here ("--repo --json") -- a genuinely missing value (repoArg - // undefined) falls through to parseRepoArg's own `!value` guard below, the single source of truth for that. - if (repoArg !== undefined && repoArg.startsWith("-")) - return { error: PURGE_USAGE }; - const repo = parseRepoArg(repoArg, PURGE_USAGE); - if ("error" in repo) - return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - return { error: `Unknown option: ${token}` }; - } - if (!options.repoFullName) - return { error: PURGE_USAGE }; - return { json: options.json, dryRun: options.dryRun, repoFullName: options.repoFullName }; -} -/** Read-only row count against an on-disk store file, for --dry-run. `{ readOnly: true }` (camelCase) is the - * only option node:sqlite recognizes for a driver-enforced read-only connection -- the lowercase `readonly` - * key is silently ignored. Never touches a store that doesn't exist yet (opening one -- even read-only -- - * requires the file to already be there; a dry run must make zero writes). */ -function countExistingRows(dbPath, countFn) { - if (!existsSync(dbPath)) - return 0; - const db = new DatabaseSync(dbPath, { readOnly: true }); - try { - return countFn(db); - } - finally { - db.close(); - } -} -function renderDryRunSummary(result) { - const purgeableLine = result.stores - .map((entry) => `${entry.store}=${entry.wouldPurge}`) - .join(", "); - return [ - `DRY RUN: would purge ${result.repoFullName} from: ${purgeableLine}. No writes were made.`, - `${ATTEMPT_LOG_NOT_PURGEABLE_NOTE} (${result.attemptLogTotalRows} total row(s) currently in attempt-log, all repos).`, - ].join("\n"); -} -export function runPurgeDryRun(parsed, options = {}) { - const resolveDbPaths = options.resolveDbPaths ?? {}; - const stores = REAL_PURGE_TARGETS.map((target) => { - const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)(); - // A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the - // per-table counts against the single read-only handle so the preview matches what a real purge removes. - // Every REAL_PURGE_TARGETS entry declares exactly one of the two, so `target.spec` is always set here. - const specs = target.specs ?? [target.spec]; - try { - const wouldPurge = countExistingRows(dbPath, (db) => specs.reduce((sum, spec) => sum + countStoreByRepo(db, spec, parsed.repoFullName), 0)); - return { store: target.name, wouldPurge }; - } - catch (error) { - return { store: target.name, wouldPurge: null, error: describeError(error) }; - } - }); - const attemptLogDbPath = (resolveDbPaths["attempt-log"] ?? resolveAttemptLogDbPath)(); - const attemptLogTotalRows = countExistingRows(attemptLogDbPath, (db) => Number(db.prepare("SELECT COUNT(*) AS count FROM attempt_log_events").get().count)); - const result = { - outcome: "dry_run", - repoFullName: parsed.repoFullName, - stores, - attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE, - attemptLogTotalRows, - }; - if (parsed.json) { - console.log(JSON.stringify(result, null, 2)); - } - else { - console.log(renderDryRunSummary(result)); - } - return 0; -} -function purgeOneStore(target, options, repoFullName) { - const ownsStore = options[target.optionKey] === undefined; - let store; - try { - store = (options[target.optionKey] ?? target.opener)(); - const purged = store.purgeByRepo(repoFullName); - return { store: target.name, purged }; - } - catch (error) { - return { store: target.name, purged: null, error: describeError(error) }; - } - finally { - if (ownsStore) - store?.close(); - } -} -function renderPurgeSummary(summary) { - const perStore = summary.stores - .map((entry) => { - if ("error" in entry) - return `${entry.store}=ERROR(${entry.error})`; - if (entry.purged === null) - return `${entry.store}=skipped`; - return `${entry.store}=${entry.purged}`; - }) - .join(", "); - return [ - `Purged ${summary.totalPurged} row(s) for ${summary.repoFullName} at ${summary.purgedAt}: ${perStore}.`, - ATTEMPT_LOG_NOT_PURGEABLE_NOTE, - ].join(" "); -} -export function runPurge(args, options = {}) { - const parsed = parsePurgeArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - return runPurgeDryRun(parsed, options); - } - const perStoreResults = REAL_PURGE_TARGETS.map((target) => purgeOneStore(target, options, parsed.repoFullName)); - perStoreResults.push({ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE }); - const totalPurged = perStoreResults.reduce((sum, entry) => sum + (entry.purged ?? 0), 0); - const hadError = perStoreResults.some((entry) => "error" in entry); - const summary = { - outcome: hadError ? "partial" : "purged", - repoFullName: parsed.repoFullName, - totalPurged, - stores: perStoreResults, - purgedAt: new Date().toISOString(), - }; - // Audit-observable by design (#5564): print the summary in BOTH the success and partial-failure case, so a - // purge -- or a purge that only partly succeeded -- is never silent. - if (parsed.json) { - console.log(JSON.stringify(summary, null, 2)); - } - else { - console.log(renderPurgeSummary(summary)); - } - return hadError ? 2 : 0; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVyZ2UtY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHVyZ2UtY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG1IQUFtSDtBQUNuSCx1R0FBdUc7QUFDdkcsZ0hBQWdIO0FBQ2hILDRHQUE0RztBQUM1RywyRUFBMkU7QUFDM0UsK0dBQStHO0FBQy9HLDhHQUE4RztBQUM5RywrR0FBK0c7QUFDL0csRUFBRTtBQUNGLDJHQUEyRztBQUMzRywrR0FBK0c7QUFDL0csMEdBQTBHO0FBQzFHLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDckMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUMzQyxPQUFPLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFOUUsT0FBTyxFQUFFLGVBQWUsRUFBRSx3QkFBd0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRTlFLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBRXZGLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSw2QkFBNkIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRTdGLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBRTVGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxxQkFBcUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRTFFLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxxQ0FBcUMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRXRILE9BQU8sRUFBRSxpQkFBaUIsRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBRXBGLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSwrQkFBK0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRXpHLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzNELE9BQU8s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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/ranked-candidates.d.ts b/packages/loopover-miner/lib/ranked-candidates.d.ts deleted file mode 100644 index e67a1f1363..0000000000 --- a/packages/loopover-miner/lib/ranked-candidates.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type RankedCandidateInput = { - repoFullName: string; - issueNumber: number; - title?: string; - htmlUrl?: string | null; - rankScore: number; - laneFit?: number; - freshness?: number; - potential?: number; - feasibility?: number; - dupRisk?: number; -}; -export type RankedCandidateRow = { - repoFullName: string; - issueNumber: number; - title: string; - htmlUrl: string | null; - rankScore: number; - laneFit: number; - freshness: number; - potential: number; - feasibility: number; - dupRisk: number; - rankedAt: string; -}; -export type RankedCandidatesSaveResult = { - count: number; - rankedAt: string; -}; -export type RankedCandidatesStore = { - dbPath: string; - saveRankedCandidates(candidates: RankedCandidateInput[], nowMs?: number): RankedCandidatesSaveResult; - listRankedCandidates(): RankedCandidateRow[]; - close(): void; -}; -export declare function resolveRankedCandidatesDbPath(env?: Record): string; -/** - * Opens the 100% local/client-side ranked-candidates snapshot store. The database only lives on this machine; - * this module never uploads, syncs, or phones home with its contents. - */ -export declare function initRankedCandidatesStore(dbPath?: string): RankedCandidatesStore; -export declare function saveRankedCandidates(candidates: RankedCandidateInput[], nowMs?: number): RankedCandidatesSaveResult; -export declare function listRankedCandidates(): RankedCandidateRow[]; -export declare function closeDefaultRankedCandidatesStore(): void; diff --git a/packages/loopover-miner/lib/ranked-candidates.js b/packages/loopover-miner/lib/ranked-candidates.js deleted file mode 100644 index 3de5df37bf..0000000000 --- a/packages/loopover-miner/lib/ranked-candidates.js +++ /dev/null @@ -1,146 +0,0 @@ -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -const defaultDbFileName = "ranked-candidates.sqlite3"; -let defaultRankedCandidatesStore = null; -export function resolveRankedCandidatesDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_RANKED_CANDIDATES_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveRankedCandidatesDbPath(), "invalid_ranked_candidates_db_path"); -} -function normalizeFiniteRankDimension(value, fallback) { - return Number.isFinite(value) ? value : fallback; -} -function normalizeCandidate(candidate) { - if (!candidate || typeof candidate !== "object") - throw new Error("invalid_ranked_candidate"); - const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : ""; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_ranked_candidate"); - const issueNumber = candidate.issueNumber; - if (!Number.isInteger(issueNumber) || issueNumber <= 0) - throw new Error("invalid_ranked_candidate"); - const rankScore = Number(candidate.rankScore); - if (!Number.isFinite(rankScore)) - throw new Error("invalid_ranked_candidate"); - return { - repoFullName: `${owner}/${repo}`, - issueNumber, - title: typeof candidate.title === "string" ? candidate.title : "", - htmlUrl: typeof candidate.htmlUrl === "string" ? candidate.htmlUrl : null, - rankScore, - // A dimension the ranker didn't supply degrades to the SAME neutral defaults opportunity-ranker.js's own - // normalizeCandidate uses for a missing signal (0 for a benefit dimension, 1 -- max risk -- for dupRisk), - // rather than silently coercing a non-finite value to 0 across the board. - laneFit: normalizeFiniteRankDimension(candidate.laneFit, 0), - freshness: normalizeFiniteRankDimension(candidate.freshness, 0), - potential: normalizeFiniteRankDimension(candidate.potential, 0), - feasibility: normalizeFiniteRankDimension(candidate.feasibility, 0), - dupRisk: normalizeFiniteRankDimension(candidate.dupRisk, 1), - }; -} -function rowToCandidate(row) { - return { - repoFullName: row.repo_full_name, - issueNumber: row.issue_number, - title: row.title, - htmlUrl: row.html_url, - rankScore: row.rank_score, - laneFit: row.lane_fit, - freshness: row.freshness, - potential: row.potential, - feasibility: row.feasibility, - dupRisk: row.dup_risk, - rankedAt: row.ranked_at, - }; -} -function asRankedCandidateDbRow(row) { - return row; -} -/** - * Opens the 100% local/client-side ranked-candidates snapshot store. The database only lives on this machine; - * this module never uploads, syncs, or phones home with its contents. - */ -export function initRankedCandidatesStore(dbPath = resolveRankedCandidatesDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const db = openLocalStoreDb(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS miner_ranked_candidates ( - repo_full_name TEXT NOT NULL, - issue_number INTEGER NOT NULL, - title TEXT NOT NULL, - html_url TEXT, - rank_score REAL NOT NULL, - lane_fit REAL NOT NULL, - freshness REAL NOT NULL, - potential REAL NOT NULL, - feasibility REAL NOT NULL, - dup_risk REAL NOT NULL, - ranked_at TEXT NOT NULL, - PRIMARY KEY (repo_full_name, issue_number) - ) - `); - // Schema-version convention (#4832): stamp the baseline. No post-baseline migrations yet -- this is a new store. - applySchemaMigrations(db, []); - const deleteAllStatement = db.prepare("DELETE FROM miner_ranked_candidates"); - const insertStatement = db.prepare(` - INSERT INTO miner_ranked_candidates - (repo_full_name, issue_number, title, html_url, rank_score, lane_fit, freshness, potential, feasibility, dup_risk, ranked_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - const listStatement = db.prepare("SELECT * FROM miner_ranked_candidates ORDER BY rank_score DESC"); - // Atomic replace: a reader between the DELETE and the INSERTs must never observe an empty table mid-write. - // node:sqlite's DatabaseSync has no `.transaction()` helper (unlike better-sqlite3) -- mirrors - // portfolio-queue.js's batchClaim: explicit BEGIN IMMEDIATE/COMMIT, ROLLBACK + rethrow on failure. - function replaceAll(normalizedCandidates, rankedAt) { - db.exec("BEGIN IMMEDIATE"); - try { - deleteAllStatement.run(); - for (const candidate of normalizedCandidates) { - insertStatement.run(candidate.repoFullName, candidate.issueNumber, candidate.title, candidate.htmlUrl, candidate.rankScore, candidate.laneFit, candidate.freshness, candidate.potential, candidate.feasibility, candidate.dupRisk, rankedAt); - } - db.exec("COMMIT"); - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - } - return { - dbPath: resolvedPath, - /** Replaces the whole snapshot wholesale with this run's ranked candidates. `nowMs` is caller-supplied - * (never reads the clock internally) so tests get a deterministic `rankedAt`. */ - saveRankedCandidates(candidates, nowMs) { - const normalized = (Array.isArray(candidates) ? candidates : []).map(normalizeCandidate); - const rankedAt = new Date(Number.isFinite(nowMs) ? nowMs : Date.now()).toISOString(); - replaceAll(normalized, rankedAt); - return { count: normalized.length, rankedAt }; - }, - /** Every candidate from the last saved run, highest rankScore first. Empty (not an error) before any - * discover run has ever saved a snapshot, or if the last run found zero candidates. */ - listRankedCandidates() { - return listStatement.all().map((row) => rowToCandidate(asRankedCandidateDbRow(row))); - }, - close() { - db.close(); - }, - }; -} -function getDefaultRankedCandidatesStore() { - defaultRankedCandidatesStore ??= initRankedCandidatesStore(); - return defaultRankedCandidatesStore; -} -export function saveRankedCandidates(candidates, nowMs) { - return getDefaultRankedCandidatesStore().saveRankedCandidates(candidates, nowMs); -} -export function listRankedCandidates() { - return getDefaultRankedCandidatesStore().listRankedCandidates(); -} -export function closeDefaultRankedCandidatesStore() { - if (!defaultRankedCandidatesStore) - return; - defaultRankedCandidatesStore.close(); - defaultRankedCandidatesStore = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmFua2VkLWNhbmRpZGF0ZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJyYW5rZWQtY2FuZGlkYXRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUseUJBQXlCLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN4RyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQXNGNUQsTUFBTSxpQkFBaUIsR0FBRywyQkFBMkIsQ0FBQztBQUN0RCxJQUFJLDRCQUE0QixHQUFpQyxJQUFJLENBQUM7QUFFdEUsTUFBTSxVQUFVLDZCQUE2QixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQ2pHLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUscUNBQXFDLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDaEcsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsNkJBQTZCLEVBQUUsRUFBRSxtQ0FBbUMsQ0FBQyxDQUFDO0FBQ2pILENBQUM7QUFFRCxTQUFTLDRCQUE0QixDQUFDLEtBQWMsRUFBRSxRQUFnQjtJQUNwRSxPQUFPLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFFLEtBQWdCLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQztBQUMvRCxDQUFDO0FBRUQsU0FBUyxrQkFBa0IsQ0FBQyxTQUErQjtJQUN6RCxJQUFJLENBQUMsU0FBUyxJQUFJLE9BQU8sU0FBUyxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLDBCQUEwQixDQUFDLENBQUM7SUFDN0YsTUFBTSxZQUFZLEdBQUcsT0FBTyxTQUFTLENBQUMsWUFBWSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/rejection-signal.d.ts b/packages/loopover-miner/lib/rejection-signal.d.ts deleted file mode 100644 index 976a21b923..0000000000 --- a/packages/loopover-miner/lib/rejection-signal.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -export type RejectionSignaledReason = "ai_usage_policy_ban" | "own_submission_rejected"; -export declare const REJECTION_REASON_AI_USAGE_POLICY_BAN = "ai_usage_policy_ban"; -export declare const REJECTION_REASON_OWN_SUBMISSION_REJECTED = "own_submission_rejected"; -export type RejectionSignalFetch = (url: string, init?: { - method?: string; - headers?: Record; -}) => Promise<{ - ok: boolean; - status: number; - json: () => Promise; - text: () => Promise; - headers?: { - get(name: string): string | null; - }; - body?: { - getReader(): { - read(): Promise<{ - done: boolean; - value?: Uint8Array; - }>; - cancel(): Promise; - releaseLock(): void; - }; - } | null; -}>; -type OwnRejectionHistorySubmission = { - pullRequestNumber?: number | null | undefined; -}; -type ListOwnSubmissions = (filter: { - repoFullName?: string; -}) => OwnRejectionHistorySubmission[]; -export interface OwnRejectionHistoryOptions { - listSubmissions?: ListOwnSubmissions; - fetchImpl?: RejectionSignalFetch; - githubToken?: string; - githubApiBaseUrl?: string; - maxRejectionHistoryChecks?: number; -} -export interface RejectionSignaledOptions extends OwnRejectionHistoryOptions { - rawContentBaseUrl?: string; -} -/** - * Resolve the SECOND `rejectionSignaled` trigger (#5655): has a prior submission from THIS miner on THIS exact - * repo already been closed/rejected? Reads this miner's own recorded submissions on the repo - * (`listRecentOwnSubmissions`, #5134), fetches each one's live PR state, and runs it through `resolveRejection` - * (#4278) -- returning `true` if ANY was closed without merge. Bounded (only the most recent - * `maxRejectionHistoryChecks` submissions with a real PR number are fetched) and fully fail-open: a wholesale - * failure to read submissions resolves to `false` (never fabricated as a rejection), and any single PR - * fetch/parse failure is skipped so it never blocks the others. Consumes both upstream modules without modifying - * either. Every dependency is injectable for testing. - */ -export declare function resolveOwnRejectionHistory(repoFullName: string, options?: OwnRejectionHistoryOptions): Promise; -/** - * Resolve whether the target repo has signaled it does not want automated/AI-authored contributions -- - * either trigger documented above. Returns `false` (never throws) on any fetch/parse failure for the policy - * docs, matching resolveAiPolicyVerdict's own fail-open default for an absent/unreadable policy doc. When a - * trigger fires, returns a trigger-specific reason string so callers can label audit-trail events accurately. - */ -export declare function resolveRejectionSignaled(repoFullName: string, options?: RejectionSignaledOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/rejection-signal.js b/packages/loopover-miner/lib/rejection-signal.js deleted file mode 100644 index e0c26d568d..0000000000 --- a/packages/loopover-miner/lib/rejection-signal.js +++ /dev/null @@ -1,156 +0,0 @@ -import { resolveAiPolicyVerdict } from "@loopover/engine"; -import { listRecentOwnSubmissions } from "./governor-state.js"; -import { resolveRejection } from "./rejection-state-machine.js"; -export const REJECTION_REASON_AI_USAGE_POLICY_BAN = "ai_usage_policy_ban"; -export const REJECTION_REASON_OWN_SUBMISSION_REJECTED = "own_submission_rejected"; -const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com"; -const MAX_POLICY_DOC_BYTES = 128 * 1024; -const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com"; -// Bound the per-call PR-status fetch fan-out (#5655): a miner with a long submission history on one repo must -// not trigger an unbounded burst of GitHub API calls on every attempt -- only the N most recent are checked. -const DEFAULT_MAX_REJECTION_HISTORY_CHECKS = 10; -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - return null; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - return { owner, repo }; -} -function normalizeOptions(options) { - return { - rawContentBaseUrl: typeof options.rawContentBaseUrl === "string" && options.rawContentBaseUrl.trim() ? options.rawContentBaseUrl.trim() : DEFAULT_RAW_CONTENT_BASE_URL, - fetchImpl: options.fetchImpl ?? fetch, - }; -} -async function readBoundedPolicyDoc(response) { - const contentLength = response.headers?.get?.("content-length"); - if (contentLength !== undefined && contentLength !== null) { - const parsedLength = Number.parseInt(contentLength, 10); - if (Number.isFinite(parsedLength) && parsedLength > MAX_POLICY_DOC_BYTES) - return null; - } - if (!response.body?.getReader) { - const text = await response.text(); - return typeof text === "string" && Buffer.byteLength(text, "utf8") <= MAX_POLICY_DOC_BYTES ? text : null; - } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let totalBytes = 0; - let text = ""; - try { - for (;;) { - const { done, value } = await reader.read(); - if (done) - break; - totalBytes += value?.byteLength ?? 0; - if (totalBytes > MAX_POLICY_DOC_BYTES) { - await reader.cancel(); - return null; - } - if (value) - text += decoder.decode(value, { stream: true }); - } - text += decoder.decode(); - return text; - } - finally { - reader.releaseLock?.(); - } -} -async function fetchPolicyDoc(target, path, resolved) { - const url = `${resolved.rawContentBaseUrl}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/HEAD/${path}`; - try { - const response = await resolved.fetchImpl(url, { method: "GET", headers: { accept: "application/json", "user-agent": "loopover-miner" } }); - if (!response.ok) - return null; - return await readBoundedPolicyDoc(response); - } - catch { - return null; - } -} -async function fetchPullRequestPayload(target, prNumber, resolved) { - const url = `${resolved.githubApiBaseUrl}/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/pulls/${prNumber}`; - const headers = { accept: "application/vnd.github+json", "user-agent": "loopover-miner" }; - if (resolved.githubToken) - headers.authorization = `Bearer ${resolved.githubToken}`; - const response = await resolved.fetchImpl(url, { method: "GET", headers }); - if (!response.ok) - return null; - return await response.json(); -} -/** - * Resolve the SECOND `rejectionSignaled` trigger (#5655): has a prior submission from THIS miner on THIS exact - * repo already been closed/rejected? Reads this miner's own recorded submissions on the repo - * (`listRecentOwnSubmissions`, #5134), fetches each one's live PR state, and runs it through `resolveRejection` - * (#4278) -- returning `true` if ANY was closed without merge. Bounded (only the most recent - * `maxRejectionHistoryChecks` submissions with a real PR number are fetched) and fully fail-open: a wholesale - * failure to read submissions resolves to `false` (never fabricated as a rejection), and any single PR - * fetch/parse failure is skipped so it never blocks the others. Consumes both upstream modules without modifying - * either. Every dependency is injectable for testing. - */ -export async function resolveOwnRejectionHistory(repoFullName, options = {}) { - const target = parseRepoFullName(repoFullName); - if (!target) - return false; - const listSubmissions = options.listSubmissions ?? listRecentOwnSubmissions; - const resolved = { - fetchImpl: options.fetchImpl ?? fetch, - githubToken: typeof options.githubToken === "string" ? options.githubToken.trim() : (process.env.GITHUB_TOKEN ?? ""), - githubApiBaseUrl: typeof options.githubApiBaseUrl === "string" && options.githubApiBaseUrl.trim() ? options.githubApiBaseUrl.trim() : DEFAULT_GITHUB_API_BASE_URL, - maxChecks: Number.isInteger(options.maxRejectionHistoryChecks) && options.maxRejectionHistoryChecks > 0 - ? options.maxRejectionHistoryChecks - : DEFAULT_MAX_REJECTION_HISTORY_CHECKS, - }; - let submissions; - try { - submissions = listSubmissions({ repoFullName }); - } - catch { - return false; // wholesale failure to read own submissions -- fail open, never fabricate a rejection - } - const checkable = (Array.isArray(submissions) ? submissions : []) - .filter((submission) => submission && Number.isInteger(submission.pullRequestNumber) && submission.pullRequestNumber > 0) - .slice(0, resolved.maxChecks); - if (checkable.length === 0) - return false; // no prior submissions on this repo -- no fetch attempted - for (const submission of checkable) { - try { - const payload = await fetchPullRequestPayload(target, submission.pullRequestNumber, resolved); - if (!payload) - continue; - // No signal (gate/duplicate context isn't available here) -- resolveRejection returns non-null only for a - // PR that is closed-without-merge, which is exactly the "was it rejected" question this check asks. - const rejection = resolveRejection(payload, undefined, { repoFullName, prNumber: submission.pullRequestNumber }); - if (rejection) - return true; - } - catch { - // Individual PR fetch/parse/classify failure -- skip this one, keep checking the rest (fail open). - } - } - return false; -} -/** - * Resolve whether the target repo has signaled it does not want automated/AI-authored contributions -- - * either trigger documented above. Returns `false` (never throws) on any fetch/parse failure for the policy - * docs, matching resolveAiPolicyVerdict's own fail-open default for an absent/unreadable policy doc. When a - * trigger fires, returns a trigger-specific reason string so callers can label audit-trail events accurately. - */ -export async function resolveRejectionSignaled(repoFullName, options = {}) { - const target = parseRepoFullName(repoFullName); - if (!target) - return false; - const resolved = normalizeOptions(options); - const aiUsage = await fetchPolicyDoc(target, "AI-USAGE.md", resolved); - const contributing = aiUsage && aiUsage.trim() ? null : await fetchPolicyDoc(target, "CONTRIBUTING.md", resolved); - const verdict = resolveAiPolicyVerdict({ aiUsage, contributing }); - // First trigger: an explicit live AI-usage-policy ban. A ban short-circuits -- no need to also check history. - if (!verdict.allowed) - return REJECTION_REASON_AI_USAGE_POLICY_BAN; - // Second trigger (#5655): a prior submission from this same miner on this exact repo was closed/rejected. - const ownHistoryRejected = await resolveOwnRejectionHistory(repoFullName, options); - return ownHistoryRejected ? REJECTION_REASON_OWN_SUBMISSION_REJECTED : false; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVqZWN0aW9uLXNpZ25hbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlamVjdGlvbi1zaWduYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDMUQsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDL0QsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFvQmhFLE1BQU0sQ0FBQyxNQUFNLG9DQUFvQyxHQUFHLHFCQUFxQixDQUFDO0FBQzFFLE1BQU0sQ0FBQyxNQUFNLHdDQUF3QyxHQUFHLHlCQUF5QixDQUFDO0FBRWxGLE1BQU0sNEJBQTRCLEdBQUcsbUNBQW1DLENBQUM7QUFDekUsTUFBTSxvQkFBb0IsR0FBRyxHQUFHLEdBQUcsSUFBSSxDQUFDO0FBQ3hDLE1BQU0sMkJBQTJCLEdBQUcsd0JBQXdCLENBQUM7QUFDN0QsOEdBQThHO0FBQzlHLDZHQUE2RztBQUM3RyxNQUFNLG9DQUFvQyxHQUFHLEVBQUUsQ0FBQztBQXFDaEQsU0FBUyxpQkFBaUIsQ0FBQyxZQUFvQjtJQUM3QyxJQUFJLE9BQU8sWUFBWSxLQUFLLFFBQVE7UUFBRSxPQUFPLElBQUksQ0FBQztJQUNsRCxNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxZQUFZLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ3JELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDO0FBQ3pCLENBQUM7QUFFRCxTQUFTLGdCQUFnQixDQUFDLE9BQWlDO0lBQ3pELE9BQU87UUFDTCxpQkFBaUIsRUFDZixPQUFPLE9BQU8sQ0FBQyxpQkFBaUIsS0FBSyxRQUFRLElBQUks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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/rejection-state-machine.d.ts b/packages/loopover-miner/lib/rejection-state-machine.d.ts deleted file mode 100644 index 574353da0b..0000000000 --- a/packages/loopover-miner/lib/rejection-state-machine.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { RejectionContext, RejectionReason } from "./rejection-templates.js"; -export type PrOutcomeFields = { - state: string | null; - merged: boolean; - mergedAt: string | null; - closedAt: string | null; -}; -export type RejectionSignal = { - gateClosed?: boolean; - supersededByDuplicate?: boolean; -}; -export type RejectionTransition = { - outcome: "disengaged"; - reason: RejectionReason; - note: string; - fields: PrOutcomeFields; -}; -/** Per-PR terminal outcome for a rejected (closed-without-merge) PR. A poller adds this to its own outcome - * vocabulary alongside ready / needs-work / open. */ -export declare const DISENGAGED_OUTCOME = "disengaged"; -/** - * Pull the terminal-outcome fields from a `GET /pulls/{n}` payload the poller already has. Pure — no API call. - * Missing/malformed fields normalize to null/false so a partial payload never throws here. - */ -export declare function extractPrOutcomeFields(prPayload: unknown): PrOutcomeFields; -/** - * True when a PR is closed WITHOUT a merge — the rejection this state machine acts on. A merged PR (even though - * GitHub also marks it `state: "closed"`) is NOT a rejection. Pure. - */ -export declare function isRejectedPr(fields: { - state?: string | null; - merged?: boolean; -} | null | undefined): boolean; -/** - * Classify a detected rejection into one of the rejection-reason buckets from the available signal. - * Precedence: an explicit gate close outranks a duplicate signal (the gate is the more specific, actionable - * cause). With neither signal, defaults to `maintainer_close_no_reason` (the documented zero-signal fallback). - * Pure. - */ -export declare function classifyRejectionReason(signal?: RejectionSignal): RejectionReason; -/** - * The full transition. Given a PR payload, an optional gate/duplicate signal, and the render context - * (`{ repoFullName, prNumber }`), decide whether the PR is a rejection and, if so, produce the disengaged - * transition: the classified reason and the rendered courtesy note (this is `renderRejectionMessage`'s first - * real caller). Returns null when the PR is not a rejection (still open, or merged) — nothing to disengage. - * Pure and deterministic; the caller persists `{ outcome, reason, note }` via its local event ledger. - */ -export declare function resolveRejection(prPayload: unknown, signal: RejectionSignal | undefined, context: RejectionContext): RejectionTransition | null; diff --git a/packages/loopover-miner/lib/rejection-state-machine.js b/packages/loopover-miner/lib/rejection-state-machine.js deleted file mode 100644 index c5ee1e4f6b..0000000000 --- a/packages/loopover-miner/lib/rejection-state-machine.js +++ /dev/null @@ -1,72 +0,0 @@ -// Rejection state machine (#4278): the missing detector + classifier that turns a closed-without-merge PR -// into a rejection-reason bucket and, for the first time, drives `renderRejectionMessage` -// (rejection-templates.js, which until now had zero callers outside its own test). Pure classification and -// content only — no GitHub calls, no network, no writes. The caller (a poller) persists the result locally. -// -// DESIGN DECISIONS (called out explicitly by #4278): -// • "disengaged" is a per-PR OUTCOME, not a per-repo run-state. A rejection is about one PR, so it belongs -// with the `manage-poll.js` outcome family (ready / needs-work / open), NOT `run-state.js`'s RUN_STATES -// (idle / discovering / planning / preparing). `DISENGAGED_OUTCOME` is defined HERE and left for a poller -// to adopt — this module deliberately does NOT mutate manage-poll.js's or run-state.js's enum as a side -// effect (the issue explicitly warns against silently expanding another module's vocabulary). -// • Zero-signal fallback: with no gate/duplicate signal, a rejection classifies as `maintainer_close_no_reason` -// — the courteous, non-assuming bucket — rather than being left unclassified, so a rejection ALWAYS renders -// a note. -// • This surfaces the PR's terminal fields from a payload the poller already fetches (ci-poller.js's -// `fetchHeadSha` GETs the full `/pulls/{n}` body, :155-163, and discards all but `head.sha`) via a pure -// extractor — no second API call, and no behavioral change to the existing fetch. -import { renderRejectionMessage } from "./rejection-templates.js"; -/** Per-PR terminal outcome for a rejected (closed-without-merge) PR. A poller adds this to its own outcome - * vocabulary alongside ready / needs-work / open. */ -export const DISENGAGED_OUTCOME = "disengaged"; -/** - * Pull the terminal-outcome fields from a `GET /pulls/{n}` payload the poller already has. Pure — no API call. - * Missing/malformed fields normalize to null/false so a partial payload never throws here. - */ -export function extractPrOutcomeFields(prPayload) { - const p = (prPayload && typeof prPayload === "object" ? prPayload : {}); - return { - state: typeof p.state === "string" ? p.state : null, - merged: p.merged === true, - mergedAt: typeof p.merged_at === "string" ? p.merged_at : null, - closedAt: typeof p.closed_at === "string" ? p.closed_at : null, - }; -} -/** - * True when a PR is closed WITHOUT a merge — the rejection this state machine acts on. A merged PR (even though - * GitHub also marks it `state: "closed"`) is NOT a rejection. Pure. - */ -export function isRejectedPr(fields) { - const f = (fields && typeof fields === "object" ? fields : {}); - return f.state === "closed" && f.merged !== true; -} -/** - * Classify a detected rejection into one of the rejection-reason buckets from the available signal. - * Precedence: an explicit gate close outranks a duplicate signal (the gate is the more specific, actionable - * cause). With neither signal, defaults to `maintainer_close_no_reason` (the documented zero-signal fallback). - * Pure. - */ -export function classifyRejectionReason(signal = {}) { - const s = (signal && typeof signal === "object" ? signal : {}); - if (s.gateClosed === true) - return "gate_close"; - if (s.supersededByDuplicate === true) - return "superseded_by_duplicate"; - return "maintainer_close_no_reason"; -} -/** - * The full transition. Given a PR payload, an optional gate/duplicate signal, and the render context - * (`{ repoFullName, prNumber }`), decide whether the PR is a rejection and, if so, produce the disengaged - * transition: the classified reason and the rendered courtesy note (this is `renderRejectionMessage`'s first - * real caller). Returns null when the PR is not a rejection (still open, or merged) — nothing to disengage. - * Pure and deterministic; the caller persists `{ outcome, reason, note }` via its local event ledger. - */ -export function resolveRejection(prPayload, signal, context) { - const fields = extractPrOutcomeFields(prPayload); - if (!isRejectedPr(fields)) - return null; - const reason = classifyRejectionReason(signal); - const note = renderRejectionMessage(reason, context); // throws on malformed context — a half-note never emits - return { outcome: DISENGAGED_OUTCOME, reason, note, fields }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVqZWN0aW9uLXN0YXRlLW1hY2hpbmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJyZWplY3Rpb24tc3RhdGUtbWFjaGluZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwwR0FBMEc7QUFDMUcsMEZBQTBGO0FBQzFGLDJHQUEyRztBQUMzRyw0R0FBNEc7QUFDNUcsRUFBRTtBQUNGLHFEQUFxRDtBQUNyRCw2R0FBNkc7QUFDN0csNEdBQTRHO0FBQzVHLDhHQUE4RztBQUM5Ryw0R0FBNEc7QUFDNUcsa0dBQWtHO0FBQ2xHLGtIQUFrSDtBQUNsSCxnSEFBZ0g7QUFDaEgsY0FBYztBQUNkLHVHQUF1RztBQUN2Ryw0R0FBNEc7QUFDNUcsc0ZBQXNGO0FBRXRGLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBc0JsRTtzREFDc0Q7QUFDdEQsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsWUFBWSxDQUFDO0FBRS9DOzs7R0FHRztBQUNILE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxTQUFrQjtJQUN2RCxNQUFNLENBQUMsR0FBRyxDQUFDLFNBQVMsSUFBSSxPQUFPLFNBQVMsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUE0QixDQUFDO0lBQ25HLE9BQU87UUFDTCxLQUFLLEVBQUUsT0FBTyxDQUFDLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUNuRCxNQUFNLEVBQUUsQ0FBQyxDQUFDLE1BQU0sS0FBSyxJQUFJO1FBQ3pCLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQyxTQUFTLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxJQUFJO1FBQzlELFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQyxTQUFTLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxJQUFJO0tBQy9ELENBQUM7QUFDSixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLFlBQVksQ0FBQyxNQUFzRTtJQUNqRyxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFnRCxDQUFDO0lBQzlHLE9BQU8sQ0FBQyxDQUFDLEtBQUssS0FBSyxRQUFRLElBQUksQ0FBQyxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUM7QUFDbkQsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLHVCQUF1QixDQUFDLFNBQTBCLEVBQUU7SUFDbEUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxNQUFNLElBQUksT0FBTyxNQUFNLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBb0IsQ0FBQztJQUNsRixJQUFJLENBQUMsQ0FBQyxVQUFVLEtBQUssSUFBSTtRQUFFLE9BQU8sWUFBWSxDQUFDO0lBQy9DLElBQUksQ0FBQyxDQUFDLHFCQUFxQixLQUFLLElBQUk7UUFBRSxPQUFPLHlCQUF5QixDQUFDO0lBQ3ZFLE9BQU8sNEJBQTRCLENBQUM7QUFDdEMsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxnQkFBZ0IsQ0FDOUIsU0FBa0IsRUFDbEIsTUFBbUMsRUFDbkMsT0FBeUI7SUFFekIsTUFBTSxNQUFNLEdBQUcsc0JBQXNCLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDakQsSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN2QyxNQUFNLE1BQU0sR0FBRyx1QkFBdUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUMvQyxNQUFNLElBQUksR0FBRyxzQkFBc0IsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyx3REFBd0Q7SUFDOUcsT0FBTyxFQUFFLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxDQUFDO0FBQy9ELENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/rejection-templates.d.ts b/packages/loopover-miner/lib/rejection-templates.d.ts deleted file mode 100644 index bbf51de490..0000000000 --- a/packages/loopover-miner/lib/rejection-templates.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -export type RejectionReason = "gate_close" | "maintainer_close_no_reason" | "superseded_by_duplicate"; -export type RejectionContext = { - repoFullName: string; - prNumber: number; -}; -/** The supported rejection-reason buckets, in declaration order. */ -export declare const REJECTION_REASONS: readonly RejectionReason[]; -/** True when the given text contains any banned private-language token. */ -export declare function containsPrivateLanguage(text: string): boolean; -/** - * Substitute every `{placeholder}` token in `template` from `values`, throwing rather than emitting a - * half-rendered note if any placeholder is unmapped (`missing_placeholder:`) or if a substituted value's - * own text still leaves what looks like an unresolved placeholder in the output (`unresolved_placeholder`) -- - * defense-in-depth against a future template-authoring bug, not a route `renderRejectionMessage` itself can hit - * today given the fixed {@link REJECTION_REASONS} vocabulary (exported so both guards are directly unit-testable - * without needing a malformed production template). Pure and deterministic. - */ -export declare function resolvePlaceholders(template: string, values: Record): string; -/** - * Render the courtesy note for a closed/rejected PR. `reason` must be one of {@link REJECTION_REASONS}; `context` - * supplies `repoFullName` (`owner/repo`) and `prNumber` (a positive integer). Throws on an unknown reason or a - * malformed context. Pure and deterministic. - */ -export declare function renderRejectionMessage(reason: RejectionReason, context: RejectionContext): string; diff --git a/packages/loopover-miner/lib/rejection-templates.js b/packages/loopover-miner/lib/rejection-templates.js deleted file mode 100644 index 57d5b6da0d..0000000000 --- a/packages/loopover-miner/lib/rejection-templates.js +++ /dev/null @@ -1,76 +0,0 @@ -// CoC-compliant rejection message templates (#2324). When one of the miner's PRs is closed/rejected, it may leave -// a single, final, human-readable local note (e.g. in a run summary or CLI output — posting anywhere is a separate -// write action, out of scope here). The note must be courteous, non-defensive, and never re-litigate the -// maintainer's decision. This module is pure content/formatting: static template strings + a deterministic -// renderer — no GitHub calls, no LLM, no network. Same inputs always render the same message. -// Templates keyed by rejection-reason bucket. Every placeholder is `{name}`; the renderer resolves the structured -// context (a PR number + a repo) and never interpolates free-form/private text. -const REASON_TEMPLATES = { - gate_close: "The automated review gate closed PR #{prNumber} on {repoFullName}. Thanks for the review — I'll address the flagged points and open a fresh PR if the change still fits.", - maintainer_close_no_reason: "PR #{prNumber} on {repoFullName} was closed by the maintainer. Thanks for taking the time to look — I'll leave it here unless you'd like me to revisit it.", - superseded_by_duplicate: "PR #{prNumber} on {repoFullName} looks superseded by other work on the same issue, so I'm closing it on my side to avoid duplication. Thanks to whoever is carrying it forward.", -}; -/** The supported rejection-reason buckets, in declaration order. */ -export const REJECTION_REASONS = Object.freeze(Object.keys(REASON_TEMPLATES)); -// Private-language tokens that must never surface in a public-facing courtesy note (mirrors the redaction set in -// `sanitizePublicComment`, src/github/commands.ts). Templates are authored clean and this is asserted in tests; -// the structured context (a PR number + a validated `owner/repo`) carries no private scoring/reward/wallet data, -// so — deliberately — no value-level redaction is applied that could mangle a legitimate repo name. -const PRIVATE_LANGUAGE = /\b(?:raw trust scores?|trust scores?|wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|payouts?|rewards?)\b/i; -/** True when the given text contains any banned private-language token. */ -export function containsPrivateLanguage(text) { - return PRIVATE_LANGUAGE.test(text); -} -// A GitHub `owner/repo`: owner is 1-39 chars of alphanumerics/hyphens starting alphanumeric; repo is -// alphanumerics/`.`/`_`/`-`. Anchored + character-class-restricted so control characters, whitespace, markup, or an -// extra `/` (e.g. `owner/repo\nextra`, `owner/`) are rejected — the note interpolates this text directly, so a -// malformed value must throw rather than leak caller-controlled display text. -const GITHUB_FULL_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/; -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const trimmed = repoFullName.trim(); - if (!GITHUB_FULL_NAME.test(trimmed)) - throw new Error("invalid_repo_full_name"); - return trimmed; -} -function normalizePrNumber(prNumber) { - if (!Number.isInteger(prNumber) || prNumber < 1) - throw new Error("invalid_pr_number"); - return prNumber; -} -/** - * Substitute every `{placeholder}` token in `template` from `values`, throwing rather than emitting a - * half-rendered note if any placeholder is unmapped (`missing_placeholder:`) or if a substituted value's - * own text still leaves what looks like an unresolved placeholder in the output (`unresolved_placeholder`) -- - * defense-in-depth against a future template-authoring bug, not a route `renderRejectionMessage` itself can hit - * today given the fixed {@link REJECTION_REASONS} vocabulary (exported so both guards are directly unit-testable - * without needing a malformed production template). Pure and deterministic. - */ -export function resolvePlaceholders(template, values) { - const rendered = template.replace(/\{(\w+)\}/g, (_match, key) => { - const value = values[key]; - if (value === undefined) - throw new Error(`missing_placeholder:${key}`); - return String(value); - }); - if (/\{[^}]+\}/.test(rendered)) - throw new Error("unresolved_placeholder"); - return rendered; -} -/** - * Render the courtesy note for a closed/rejected PR. `reason` must be one of {@link REJECTION_REASONS}; `context` - * supplies `repoFullName` (`owner/repo`) and `prNumber` (a positive integer). Throws on an unknown reason or a - * malformed context. Pure and deterministic. - */ -export function renderRejectionMessage(reason, context) { - const template = REASON_TEMPLATES[reason]; - if (template === undefined) - throw new Error("invalid_rejection_reason"); - const values = { - repoFullName: normalizeRepoFullName(context.repoFullName), - prNumber: normalizePrNumber(context.prNumber), - }; - return resolvePlaceholders(template, values); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVqZWN0aW9uLXRlbXBsYXRlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlamVjdGlvbi10ZW1wbGF0ZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsa0hBQWtIO0FBQ2xILG1IQUFtSDtBQUNuSCx5R0FBeUc7QUFDekcsMkdBQTJHO0FBQzNHLDhGQUE4RjtBQVM5RixrSEFBa0g7QUFDbEgsZ0ZBQWdGO0FBQ2hGLE1BQU0sZ0JBQWdCLEdBQW9DO0lBQ3hELFVBQVUsRUFDUiwwS0FBMEs7SUFDNUssMEJBQTBCLEVBQ3hCLDRKQUE0SjtJQUM5Six1QkFBdUIsRUFDckIsaUxBQWlMO0NBQ3BMLENBQUM7QUFFRixvRUFBb0U7QUFDcEUsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQStCLE1BQU0sQ0FBQyxNQUFNLENBQ3hFLE1BQU0sQ0FBQyxJQUFJLENBQUMsZ0JBQWdCLENBQXNCLENBQ25ELENBQUM7QUFFRixpSEFBaUg7QUFDakgsZ0hBQWdIO0FBQ2hILGlIQUFpSDtBQUNqSCxvR0FBb0c7QUFDcEcsTUFBTSxnQkFBZ0IsR0FDcEIsaUhBQWlILENBQUM7QUFFcEgsMkVBQTJFO0FBQzNFLE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxJQUFZO0lBQ2xELE9BQU8sZ0JBQWdCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQ3JDLENBQUM7QUFFRCxxR0FBcUc7QUFDckcsb0hBQW9IO0FBQ3BILHFIQUFxSDtBQUNySCw4RUFBOEU7QUFDOUUsTUFBTSxnQkFBZ0IsR0FBRyw0REFBNEQsQ0FBQztBQUV0RixTQUFTLHFCQUFxQixDQUFDLFlBQXFCO0lBQ2xELElBQUksT0FBTyxZQUFZLEtBQUssUUFBUTtRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUNoRixNQUFNLE9BQU8sR0FBRyxZQUFZLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDcEMsSUFBSSxDQUFDLGdCQUFnQixDQUFDLElBQUksQ0FBQyxPQUFPLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDL0UsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQztBQUVELFNBQVMsaUJBQWlCLENBQUMsUUFBaUI7SUFDMUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLElBQUssUUFBbUIsR0FBRyxDQUFDO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBQ2xHLE9BQU8sUUFBa0IsQ0FBQztBQUM1QixDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxRQUFnQixFQUFFLE1BQXVDO0lBQzNGLE1BQU0sUUFBUSxHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLENBQUMsTUFBTSxFQUFFLEdBQUcsRUFBRSxFQUFFO1FBQzlELE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMxQixJQUFJLEtBQUssS0FBSyxTQUFTO1lBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsR0FBRyxFQUFFLENBQUMsQ0FBQztRQUN2RSxPQUFPLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN2QixDQUFDLENBQUMsQ0FBQztJQUNILElBQUksV0FBVyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDMUUsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsc0JBQXNCLENBQUMsTUFBdUIsRUFBRSxPQUF5QjtJQUN2RixNQUFNLFFBQVEsR0FBRyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUMxQyxJQUFJLFFBQVEsS0FBSyxTQUFTO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQywwQkFBMEIsQ0FBQyxDQUFDO0lBQ3hFLE1BQU0sTUFBTSxHQUFvQztRQUM5QyxZQUFZLEVBQUUscUJBQXFCLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUN6RCxRQUFRLEVBQUUsaUJBQWlCLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQztLQUM5QyxDQUFDO0lBQ0YsT0FBTyxtQkFBbUIsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7QUFDL0MsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/replay-objective-anchor.d.ts b/packages/loopover-miner/lib/replay-objective-anchor.d.ts deleted file mode 100644 index aed1615892..0000000000 --- a/packages/loopover-miner/lib/replay-objective-anchor.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -export type ChangeKind = "feature" | "fix" | "refactor" | "docs" | "test" | "chore" | "perf" | "build" | "ci" | "style" | "other"; -export declare const CHANGE_KINDS: readonly ChangeKind[]; -export declare const MODULE_OVERLAP_WEIGHT = 0.7; -export declare const CHANGE_KIND_WEIGHT = 0.3; -export type ReplayPlanInput = { - pathsTouched?: unknown; - changeKind?: unknown; - title?: unknown; -}; -export type RevealedHistoryEntry = { - pathsTouched?: unknown; - changeKind?: unknown; - title?: unknown; -}; -export type ReplayTargetFeatures = { - modules: string[]; - changeKind: ChangeKind; -}; -export type RevealedFeatures = { - modules: string[]; - changeKinds: ChangeKind[]; -}; -export type ObjectiveAnchorBreakdown = { - score: number; - moduleOverlap: number; - changeKindMatch: 0 | 1; - replayChangeKind: ChangeKind; - revealedChangeKinds: ChangeKind[]; - sharedModules: string[]; - replayOnlyModules: string[]; - revealedOnlyModules: string[]; -}; -export type ObjectiveAnchorResult = ObjectiveAnchorBreakdown & { - replayFeatures: ReplayTargetFeatures; - revealedFeatures: RevealedFeatures; -}; -export declare function classifyChangeKind(value: unknown): ChangeKind; -export declare function extractReplayTargetFeatures(plan: ReplayPlanInput | null | undefined): ReplayTargetFeatures; -export declare function extractRevealedFeatures(history: readonly unknown[] | RevealedHistoryEntry | null | undefined): RevealedFeatures; -export declare function scoreObjectiveAnchor(replayFeatures: { - modules?: unknown; - changeKind?: unknown; -} | null | undefined, revealedFeatures: { - modules?: unknown; - changeKinds?: unknown; -} | null | undefined): ObjectiveAnchorBreakdown; -export declare function computeObjectiveAnchor(input: { - replayPlan?: ReplayPlanInput | null; - revealedHistory?: RevealedHistoryEntry[] | RevealedHistoryEntry | null; -} | null | undefined): ObjectiveAnchorResult; diff --git a/packages/loopover-miner/lib/replay-objective-anchor.js b/packages/loopover-miner/lib/replay-objective-anchor.js deleted file mode 100644 index 8bf2ba93ab..0000000000 --- a/packages/loopover-miner/lib/replay-objective-anchor.js +++ /dev/null @@ -1,178 +0,0 @@ -// Deterministic structural "objective-anchor" score for the historical-replay calibration harness (#3012). -// -// Once a replay run produces a plan/PR against a frozen snapshot, half of the calibration score is meant to -// come from a deterministic, auditable structural comparison rather than an LLM judgment. This module is that -// structural half: it compares what the miner's replayed output *targeted* (modules touched + change kind) -// against what the revealed post-T history *actually* changed, and returns a reproducible `[0, 1]` score plus -// a full audit breakdown. There is no model call in this path — given the same two feature sets it is -// byte-for-byte reproducible. -// Fixed change-kind vocabulary. Conventional-Commit types collapse onto these buckets; anything unrecognized -// degrades to "other" so a novel prefix lowers the signal instead of throwing. -export const CHANGE_KINDS = Object.freeze([ - "feature", - "fix", - "refactor", - "docs", - "test", - "chore", - "perf", - "build", - "ci", - "style", - "other", -]); -const CONVENTIONAL_TYPE_TO_KIND = new Map([ - ["feat", "feature"], - ["feature", "feature"], - ["fix", "fix"], - ["bugfix", "fix"], - ["refactor", "refactor"], - ["docs", "docs"], - ["doc", "docs"], - ["test", "test"], - ["tests", "test"], - ["chore", "chore"], - ["perf", "perf"], - ["build", "build"], - ["ci", "ci"], - ["style", "style"], -]); -// Fixed weights for the two structural components. They sum to 1 so the composed score stays in [0, 1]. -export const MODULE_OVERLAP_WEIGHT = 0.7; -export const CHANGE_KIND_WEIGHT = 0.3; -const SCORE_PRECISION = 1e4; -function roundScore(value) { - return Math.round(value * SCORE_PRECISION) / SCORE_PRECISION; -} -// A path's "module" is its directory (everything before the final slash); a bare filename is its own module. -// Grouping by directory is what makes two different files in one directory a *partial* overlap, not a miss. -function pathToModule(path) { - const trimmed = path.trim().replace(/^(?:\.\/)+/, "").replace(/\/+$/, ""); - if (!trimmed) - return null; - const slash = trimmed.lastIndexOf("/"); - return slash === -1 ? trimmed : trimmed.slice(0, slash); -} -function normalizeModules(pathsTouched) { - if (!Array.isArray(pathsTouched)) - return []; - const modules = new Set(); - for (const entry of pathsTouched) { - if (typeof entry !== "string") - continue; - const module = pathToModule(entry); - if (module) - modules.add(module); - } - return [...modules].sort(); -} -function normalizeKindList(value) { - if (!Array.isArray(value)) - return []; - const kinds = new Set(); - for (const entry of value) { - if (typeof entry === "string" && isChangeKind(entry)) - kinds.add(entry); - } - return [...kinds].sort(); -} -function isChangeKind(value) { - return CHANGE_KINDS.includes(value); -} -function normalizeModuleList(value) { - if (!Array.isArray(value)) - return []; - const modules = new Set(); - for (const entry of value) { - if (typeof entry === "string" && entry) - modules.add(entry); - } - return [...modules].sort(); -} -// Deterministically map a Conventional-Commit-style subject (`feat(scope)!: …`) to a change-kind bucket. -// Missing prefix, unknown type, or non-string input all resolve to "other" rather than throwing. -export function classifyChangeKind(value) { - if (typeof value !== "string") - return "other"; - const match = /^\s*([A-Za-z]+)\s*(?:\([^)]*\))?\s*!?\s*:/.exec(value); - if (!match) - return "other"; - return CONVENTIONAL_TYPE_TO_KIND.get(match[1].toLowerCase()) ?? "other"; -} -function resolveChangeKind(entry) { - if (entry && typeof entry.changeKind === "string") { - const explicit = entry.changeKind.trim().toLowerCase(); - if (isChangeKind(explicit)) - return explicit; - } - return classifyChangeKind(entry?.title); -} -// Structural features of the miner's replayed plan/PR: the sorted, de-duplicated set of modules it targeted -// and its single change kind (explicit `changeKind` wins; otherwise classified from `title`). -export function extractReplayTargetFeatures(plan) { - return { - modules: normalizeModules(plan?.pathsTouched), - changeKind: resolveChangeKind(plan), - }; -} -// Structural features of the revealed post-T history. The history is a list of commits/PRs (a single object -// is tolerated as a one-element list); modules are unioned and change kinds collected into a set, since the -// revealed side legitimately spans several changes. -export function extractRevealedFeatures(history) { - const entries = Array.isArray(history) ? history : history ? [history] : []; - const modules = new Set(); - const changeKinds = new Set(); - for (const entry of entries) { - if (!entry || typeof entry !== "object") - continue; - const record = entry; - for (const module of normalizeModules(record.pathsTouched)) - modules.add(module); - changeKinds.add(resolveChangeKind(record)); - } - return { - modules: [...modules].sort(), - changeKinds: [...changeKinds].sort(), - }; -} -// Deterministic objective-anchor score from two already-extracted feature sets. No LLM, no clock, no -// randomness — identical inputs always yield an identical breakdown. A zero-overlap comparison (disjoint -// modules and a change kind the revealed side never shows) resolves to the score floor `0`, never an error. -export function scoreObjectiveAnchor(replayFeatures, revealedFeatures) { - const replayModules = normalizeModuleList(replayFeatures?.modules); - const revealedModules = normalizeModuleList(revealedFeatures?.modules); - const replayChangeKind = typeof replayFeatures?.changeKind === "string" && isChangeKind(replayFeatures.changeKind) - ? replayFeatures.changeKind - : "other"; - const revealedChangeKinds = normalizeKindList(revealedFeatures?.changeKinds); - const replaySet = new Set(replayModules); - const revealedSet = new Set(revealedModules); - const sharedModules = replayModules.filter((module) => revealedSet.has(module)); - const replayOnlyModules = replayModules.filter((module) => !revealedSet.has(module)); - const revealedOnlyModules = revealedModules.filter((module) => !replaySet.has(module)); - const unionSize = replayModules.length + revealedModules.length - sharedModules.length; - const moduleOverlap = unionSize === 0 ? 0 : sharedModules.length / unionSize; - const changeKindMatch = revealedChangeKinds.includes(replayChangeKind) ? 1 : 0; - return { - score: roundScore(MODULE_OVERLAP_WEIGHT * moduleOverlap + CHANGE_KIND_WEIGHT * changeKindMatch), - moduleOverlap: roundScore(moduleOverlap), - changeKindMatch, - replayChangeKind, - revealedChangeKinds, - sharedModules, - replayOnlyModules, - revealedOnlyModules, - }; -} -// One-shot entry point: extract both sides, score them, and return the score together with the extracted -// feature sets so a low score is auditable after the fact without re-running the extraction. -export function computeObjectiveAnchor(input) { - const replayFeatures = extractReplayTargetFeatures(input?.replayPlan); - const revealedFeatures = extractRevealedFeatures(input?.revealedHistory); - return { - ...scoreObjectiveAnchor(replayFeatures, revealedFeatures), - replayFeatures, - revealedFeatures, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LW9iamVjdGl2ZS1hbmNob3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJyZXBsYXktb2JqZWN0aXZlLWFuY2hvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyR0FBMkc7QUFDM0csRUFBRTtBQUNGLDRHQUE0RztBQUM1Ryw4R0FBOEc7QUFDOUcsMkdBQTJHO0FBQzNHLDhHQUE4RztBQUM5RyxzR0FBc0c7QUFDdEcsOEJBQThCO0FBZTlCLDZHQUE2RztBQUM3RywrRUFBK0U7QUFDL0UsTUFBTSxDQUFDLE1BQU0sWUFBWSxHQUEwQixNQUFNLENBQUMsTUFBTSxDQUFDO0lBQy9ELFNBQVM7SUFDVCxLQUFLO0lBQ0wsVUFBVTtJQUNWLE1BQU07SUFDTixNQUFNO0lBQ04sT0FBTztJQUNQLE1BQU07SUFDTixPQUFPO0lBQ1AsSUFBSTtJQUNKLE9BQU87SUFDUCxPQUFPO0NBQ1IsQ0FBQyxDQUFDO0FBRUgsTUFBTSx5QkFBeUIsR0FBRyxJQUFJLEdBQUcsQ0FBcUI7SUFDNUQsQ0FBQyxNQUFNLEVBQUUsU0FBUyxDQUFDO0lBQ25CLENBQUMsU0FBUyxFQUFFLFNBQVMsQ0FBQztJQUN0QixDQUFDLEtBQUssRUFBRSxLQUFLLENBQUM7SUFDZCxDQUFDLFFBQVEsRUFBRSxLQUFLLENBQUM7SUFDakIsQ0FBQyxVQUFVLEVBQUUsVUFBVSxDQUFDO0lBQ3hCLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQztJQUNoQixDQUFDLEtBQUssRUFBRSxNQUFNLENBQUM7SUFDZixDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUM7SUFDaEIsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDO0lBQ2pCLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQztJQUNsQixDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUM7SUFDaEIsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDO0lBQ2xCLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQztJQUNaLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQztDQUNuQixDQUFDLENBQUM7QUFFSCx3R0FBd0c7QUFDeEcsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsR0FBRyxDQUFDO0FBQ3pDLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLEdBQUcsQ0FBQztBQUV0QyxNQUFNLGVBQWUsR0FBRyxHQUFHLENBQUM7QUFFNUIsU0FBUyxVQUFVLENBQUMsS0FBYTtJQUMvQixPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxHQUFHLGVBQWUsQ0FBQyxHQUFHLGVBQWUsQ0FBQztBQUMvRCxDQUFDO0FBRUQsNkdBQTZHO0FBQzdHLDRHQUE0RztBQUM1RyxTQUFTLFlBQVksQ0FBQyxJQUFZO0lBQ2hDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDMUUsSUFBSSxDQUFDLE9BQU87UUFBRSxPQUFPLElBQUksQ0FBQztJQUMxQixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ3ZDLE9BQU8sS0FBSyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEtBQUssQ0FBQyxDQUFDO0FBQzFELENBQUM7QUFFRCxTQUFTLGdCQUFnQixDQUFDLFlBQXFCO0lBQzdDLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQzVDLE1BQU0sT0FBTyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFDbEMsS0FBSyxNQUFNLEtBQUssSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUNqQyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVE7WUFBRSxTQUFTO1FBQ3hDLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUNuQyxJQUFJLE1BQU07WUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ2xDLENBQUM7SUFDRCxPQUFPLENBQUMsR0FBRyxPQUFPLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUM3QixDQUFDO0FBRUQsU0FBUyxpQkFBaUIsQ0FBQyxLQUFjO0lBQ3ZDLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQ3JDLE1BQU0sS0FBSyxHQUFHLElBQUksR0FBRyxFQUFjLENBQUM7SUFDcEMsS0FBSyxNQUFNLEtBQUssSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUMxQixJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxZQUFZLENBQUMsS0FBSyxDQUFDO1lBQUUsS0FBSyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN6RSxDQUFDO0lBQ0QsT0FBTyxDQUFDLEdBQUcsS0FBSyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDM0IsQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEtBQWE7SUFDakMsT0FBUSxZQUFrQyxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUM3RCxDQUFDO0FBRUQsU0FBUyxtQkFBbUIsQ0FBQyxLQUFjO0lBQ3pDLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQ3JDLE1BQU0sT0FBTyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFDbEMsS0FBSyxNQUFNLEtBQUssSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUMxQixJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLO1lBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUM3RCxDQUFDO0lBQ0QsT0FBTyxDQUFDLEdBQUcsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDN0IsQ0FBQztBQXdDRCx5R0FBeUc7QUFDekcsaUdBQWlHO0FBQ2pHLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxLQUFjO0lBQy9DLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sT0FBTyxDQUFDO0lBQzlDLE1BQU0sS0FBSyxHQUFHLDJDQUEyQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN0RSxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sT0FBTyxDQUFDO0lBQzNCLE9BQU8seUJBQXlCLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQztBQUMzRSxDQUFDO0FBRUQsU0FBUyxpQkFBaUIsQ0FBQyxLQUFtRTtJQUM1RixJQUFJLEtBQUssSUFBSSxPQUFPLEtBQUssQ0FBQyxVQUFVLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDbEQsTUFBTSxRQUFRLEdBQUcsS0FBSyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUN2RCxJQUFJLFlBQVksQ0FBQyxRQUFRLENBQUM7WUFBRSxPQUFPLFFBQVEsQ0FBQztJQUM5QyxDQUFDO0lBQ0QsT0FBTyxrQkFBa0IsQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDMUMsQ0FBQztBQUVELDRHQUE0RztBQUM1Ryw4RkFBOEY7QUFDOUYsTUFBTSxVQUFVLDJCQUEyQixDQUFDLElBQXdDO0lBQ2xGLE9BQU87UUFDTCxPQUFPLEVBQUUsZ0JBQWdCLENBQUMsSUFBSSxFQUFFLFlBQVksQ0FBQztRQUM3QyxVQUFVLEVBQUUsaUJBQWlCLENBQUMsSUFBSSxDQUFDO0tBQ3BDLENBQUM7QUFDSixDQUFDO0FBRUQsNEdBQTRHO0FBQzVHLDRHQUE0RztBQUM1RyxvREFBb0Q7QUFDcEQsTUFBTSxVQUFVLHVCQUF1QixDQUFDLE9BQXFFO0lBQzNHLE1BQU0sT0FBTyxHQUFjLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDdkYsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztJQUNsQyxNQUFNLFdBQVcsR0FBRyxJQUFJLEdBQUcsRUFBYyxDQUFDO0lBQzFDLEtBQUssTUFBTSxLQUFLLElBQUksT0FBTyxFQUFFLENBQUM7UUFDNUIsSUFBSSxDQUFDLEtBQUssSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRO1lBQUUsU0FBUztRQUNsRCxNQUFNLE1BQU0sR0FBRyxLQUE2QixDQUFDO1FBQzdDLEtBQUssTUFBTSxNQUFNLElBQUksZ0JBQWdCLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQztZQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDaEYsV0FBVyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFDRCxPQUFPO1FBQ0wsT0FBTyxFQUFFLENBQUMsR0FBRyxPQUFPLENBQUMsQ0FBQyxJQUFJLEVBQUU7UUFDNUIsV0FBVyxFQUFFLENBQUMsR0FBRyxXQUFXLENBQUMsQ0FBQyxJQUFJLEVBQUU7S0FDckMsQ0FBQztBQUNKLENBQUM7QUFFRCxxR0FBcUc7QUFDckcseUdBQXlHO0FBQ3pHLDRHQUE0RztBQUM1RyxNQUFNLFVBQVUsb0JBQW9CLENBQ2xDLGNBQThFLEVBQzlFLGdCQUFpRjtJQUVqRixNQUFNLGFBQWEsR0FBRyxtQkFBbUIsQ0FBQyxjQUFjLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDbkUsTUFBTSxlQUFlLEdBQUcsbUJBQW1CLENBQUMsZ0JBQWdCLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdkUsTUFBTSxnQkFBZ0IsR0FDcEIsT0FBTyxjQUFjLEVBQUUsVUFBVSxLQUFLLFFBQVEsSUFBSSxZQUFZLENBQUMsY0FBYyxDQUFDLFVBQVUsQ0FBQztRQUN2RixDQUFDLENBQUMsY0FBYyxDQUFDLFVBQVU7UUFDM0IsQ0FBQyxDQUFDLE9BQU8sQ0FBQztJQUNkLE1BQU0sbUJBQW1CLEdBQUcsaUJBQWlCLENBQUMsZ0JBQWdCLEVBQUUsV0FBVyxDQUFDLENBQUM7SUFFN0UsTUFBTSxTQUFTLEdBQUcsSUFBSSxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7SUFDekMsTUFBTSxXQUFXLEdBQUcsSUFBSSxHQUFHLENBQUMsZUFBZSxDQUFDLENBQUM7SUFDN0MsTUFBTSxhQUFhLEdBQUcsYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsV0FBVyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDO0lBQ2hGLE1BQU0saUJBQWlCLEdBQUcsYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDckYsTUFBTSxtQkFBbUIsR0FBRyxlQUFlLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztJQUV2RixNQUFNLFNBQVMsR0FBRyxhQUFhLENBQUMsTUFBTSxHQUFHLGVBQWUsQ0FBQyxNQUFNLEdBQUcsYUFBYSxDQUFDLE1BQU0sQ0FBQztJQUN2RixNQUFNLGFBQWEsR0FBRyxTQUFTLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxNQUFNLEdBQUcsU0FBUyxDQUFDO0lBQzdFLE1BQU0sZUFBZSxHQUFVLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUV0RixPQUFPO1FBQ0wsS0FBSyxFQUFFLFVBQVUsQ0FBQyxxQkFBcUIsR0FBRyxhQUFhLEdBQUcsa0JBQWtCLEdBQUcsZUFBZSxDQUFDO1FBQy9GLGFBQWEsRUFBRSxVQUFVLENBQUMsYUFBYSxDQUFDO1FBQ3hDLGVBQWU7UUFDZixnQkFBZ0I7UUFDaEIsbUJBQW1CO1FBQ25CLGFBQWE7UUFDYixpQkFBaUI7UUFDakIsbUJBQW1CO0tBQ3BCLENBQUM7QUFDSixDQUFDO0FBRUQseUdBQXlHO0FBQ3pHLDZGQUE2RjtBQUM3RixNQUFNLFVBQVUsc0JBQXNCLENBQ3BDLEtBTWE7SUFFYixNQUFNLGNBQWMsR0FBRywyQkFBMkIsQ0FBQyxLQUFLLEVBQUUsVUFBVSxDQUFDLENBQUM7SUFDdEUsTUFBTSxnQkFBZ0IsR0FBRyx1QkFBdUIsQ0FBQyxLQUFLLEVBQUUsZUFBZSxDQUFDLENBQUM7SUFDekUsT0FBTztRQUNMLEdBQUcsb0JBQW9CLENBQUMsY0FBYyxFQUFFLGdCQUFnQixDQUFDO1FBQ3pELGNBQWM7UUFDZCxnQkFBZ0I7S0FDakIsQ0FBQztBQUNKLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/replay-snapshot.d.ts b/packages/loopover-miner/lib/replay-snapshot.d.ts deleted file mode 100644 index 8f402ad52c..0000000000 --- a/packages/loopover-miner/lib/replay-snapshot.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { WorktreeExecFn, WorktreeRemoveResult } from "@loopover/engine"; -export type ReplaySnapshotCommit = { - sha: string; - date: string; - subject: string; -}; -export type ReplaySnapshotTag = { - name: string; - date: string; - targetSha: string; -}; -export type ReplaySnapshotReadme = { - filename: string; - content: string; -}; -export type ReplaySnapshot = { - repoFullName: string; - commitSha: string; - worktreePath: string; - targetDate: string; - commits: ReplaySnapshotCommit[]; - tags: ReplaySnapshotTag[]; - readme: ReplaySnapshotReadme | null; - exportedAt: string; -}; -export type ReplaySnapshotStore = { - dbPath: string; - getSnapshot(repoFullName: string, commitSha: string): ReplaySnapshot | null; - saveSnapshot(snapshot: Omit): ReplaySnapshot; - close(): void; -}; -export declare function resolveReplaySnapshotDbPath(env?: Record): string; -/** Worktree exports live under this dir inside the repo, mirroring worktree-allocator.ts's WORKTREE_SUBDIR. */ -export declare const REPLAY_SNAPSHOT_SUBDIR = ".loopover-replay-snapshots"; -/** PURE: the deterministic on-disk location for a (repo, commit) replay export -- same pair -> same path. */ -export declare function planReplaySnapshotPath(input: { - repoPath: string; - commitSha: string; -}): string; -/** PURE: fails fast (throws) if any exported commit or tag carries a date LATER than the target commit's own - * date. Returns nothing on success. */ -export declare function validateSnapshotFreshness(input: { - targetDate: string; - commits: ReplaySnapshotCommit[]; - tags: ReplaySnapshotTag[]; -}): void; -export declare function openReplaySnapshotStore(dbPath?: string): ReplaySnapshotStore; -export declare function closeDefaultReplaySnapshotStore(): void; -/** - * Export a frozen, reproducible replay snapshot for (repoFullName, commitSha): a detached working-tree checkout - * at that commit plus a context bundle (commit history, reachable tags, README-at-commit). Returns the CACHED - * snapshot without touching git again if one already exists for this exact (repo, commit) pair. - */ -export declare function exportReplaySnapshot(input: { - repoPath: string; - repoFullName: string; - commitSha: string; -}, deps: { - exec: WorktreeExecFn; - store?: ReplaySnapshotStore; -}): Promise; -/** Tear down a replay snapshot's working-tree export (the cached context-bundle row is left in place -- it is - * cheap, commit-keyed, and re-usable even after the on-disk tree is removed; only re-adding the worktree would - * require the tree again, which is out of this function's scope). */ -export declare function removeReplaySnapshotWorktree(exec: WorktreeExecFn, repoPath: string, worktreePath: string): Promise; diff --git a/packages/loopover-miner/lib/replay-snapshot.js b/packages/loopover-miner/lib/replay-snapshot.js deleted file mode 100644 index 6c9b28c2a3..0000000000 --- a/packages/loopover-miner/lib/replay-snapshot.js +++ /dev/null @@ -1,245 +0,0 @@ -import { join } from "node:path"; -import { removeWorktree } from "@loopover/engine"; -import { openLocalStoreAdapter, resolveLocalStoreDbPath, normalizeLocalStoreDbPath } from "./local-store.js"; -const defaultDbFileName = "replay-snapshot.sqlite3"; -let defaultDb = null; -export function resolveReplaySnapshotDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_REPLAY_SNAPSHOT_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveReplaySnapshotDbPath(), "invalid_replay_snapshot_db_path"); -} -const FIELD_SEP = "\x1f"; -const README_NAME_PATTERN = /^readme(\.\w+)?$/i; -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeCommitSha(commitSha) { - if (typeof commitSha !== "string" || !commitSha.trim()) - throw new Error("invalid_commit_sha"); - return commitSha.trim(); -} -/** Worktree exports live under this dir inside the repo, mirroring worktree-allocator.ts's WORKTREE_SUBDIR. */ -export const REPLAY_SNAPSHOT_SUBDIR = ".loopover-replay-snapshots"; -/** PURE: the deterministic on-disk location for a (repo, commit) replay export -- same pair -> same path. */ -export function planReplaySnapshotPath(input) { - const commitSha = normalizeCommitSha(input.commitSha); - return join(input.repoPath, REPLAY_SNAPSHOT_SUBDIR, commitSha); -} -function assertExecResult(result, description) { - if (result.code !== 0) { - const detail = (result.stderr ?? "").trim() || `exit_${result.code}`; - throw new Error(`${description}: ${detail}`); - } - return result.stdout ?? ""; -} -/** Detached checkout at commitSha via `git worktree add --detach` -- never creates a branch, never touches the - * caller's own checkout. Idempotent in effect: `git worktree add` itself fails if the path already has a - * worktree, which callers avoid by checking the store cache first (see exportReplaySnapshot). */ -async function addDetachedWorktree(exec, repoPath, worktreePath, commitSha) { - const result = await exec("git", ["worktree", "add", "--detach", worktreePath, commitSha], { cwd: repoPath }); - assertExecResult(result, "git_worktree_add_failed"); -} -async function readTargetCommitDate(exec, repoPath, commitSha) { - const result = await exec("git", ["log", "-1", "--format=%cI", commitSha], { cwd: repoPath }); - const stdout = assertExecResult(result, "git_log_target_failed").trim(); - if (!stdout) - throw new Error(`git_log_target_failed: no commit found for ${commitSha}`); - return stdout; -} -async function readCommitHistory(exec, repoPath, commitSha) { - const result = await exec("git", ["log", commitSha, `--format=%H${FIELD_SEP}%cI${FIELD_SEP}%s`], { cwd: repoPath }); - const stdout = assertExecResult(result, "git_log_history_failed"); - return stdout - .split("\n") - .filter((line) => line.length > 0) - .map((line) => { - const [sha, date, subject] = line.split(FIELD_SEP); - return { sha: sha, date: date, subject: subject ?? "" }; - }); -} -// Lightweight tags have no tag object of their own, so `%(creatordate)` falls back to the POINTED-TO commit's -// date rather than a genuine tag-creation date -- git has no record of when a lightweight tag was actually -// created at all. That means a lightweight tag added long after T, but pointing at an ancestor of T, would -// silently pass validateSnapshotFreshness's date check every time (its reported "date" is always <= T's, by -// construction of --merged). Since this can never be verified, lightweight tags are excluded from the export -// entirely -- `%(objecttype)` is "tag" only for an annotated tag's own tag object, "commit" for a lightweight -// tag's direct target, which is how the two are told apart. -async function readReachableTags(exec, repoPath, commitSha) { - const result = await exec("git", ["tag", "--merged", commitSha, `--format=%(refname:short)${FIELD_SEP}%(creatordate:iso-strict)${FIELD_SEP}%(objectname)${FIELD_SEP}%(objecttype)`], { cwd: repoPath }); - const stdout = assertExecResult(result, "git_tag_merged_failed"); - return stdout - .split("\n") - .filter((line) => line.length > 0) - .map((line) => { - const [name, date, targetSha, objectType] = line.split(FIELD_SEP); - return { name: name, date: date, targetSha: targetSha, objectType }; - }) - .filter((tag) => tag.objectType === "tag") - .map(({ objectType, ...tag }) => tag); -} -/** Finds the repo-root README (any casing/extension) at commitSha and returns its content, or null if none - * exists at that commit. Uses `git ls-tree` to find the real filename rather than guessing a fixed spelling - * list. */ -async function readReadmeAtCommit(exec, repoPath, commitSha) { - const listing = await exec("git", ["ls-tree", "--name-only", commitSha], { cwd: repoPath }); - const stdout = assertExecResult(listing, "git_ls_tree_failed"); - const filename = stdout - .split("\n") - .map((line) => line.trim()) - .find((line) => README_NAME_PATTERN.test(line)); - if (!filename) - return null; - const shown = await exec("git", ["show", `${commitSha}:${filename}`], { cwd: repoPath }); - const content = assertExecResult(shown, "git_show_readme_failed"); - return { filename, content }; -} -/** PURE: fails fast (throws) if any exported commit or tag carries a date LATER than the target commit's own - * date. Returns nothing on success. */ -export function validateSnapshotFreshness(input) { - const targetMs = Date.parse(input.targetDate); - const violations = []; - for (const commit of input.commits) { - if (Date.parse(commit.date) > targetMs) - violations.push(`commit ${commit.sha} dated ${commit.date} is after target ${input.targetDate}`); - } - for (const tag of input.tags) { - if (Date.parse(tag.date) > targetMs) - violations.push(`tag ${tag.name} dated ${tag.date} is after target ${input.targetDate}`); - } - if (violations.length > 0) - throw new Error(`replay_snapshot_freshness_violation: ${violations.join("; ")}`); -} -function rowToSnapshot(row) { - return { - repoFullName: row.repo_full_name, - commitSha: row.commit_sha, - worktreePath: row.worktree_path, - targetDate: row.target_date, - commits: JSON.parse(row.commits_json), - tags: JSON.parse(row.tags_json), - readme: row.readme_filename ? { filename: row.readme_filename, content: row.readme_content } : null, - exportedAt: row.exported_at, - }; -} -export function openReplaySnapshotStore(dbPath = resolveReplaySnapshotDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - // Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - // while schema creation still uses the underlying DatabaseSync until that helper is migrated. Public API - // stays synchronous so callers need no async cascade in this part-1 slice. - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS replay_snapshots ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_full_name TEXT NOT NULL, - commit_sha TEXT NOT NULL, - worktree_path TEXT NOT NULL, - target_date TEXT NOT NULL, - commits_json TEXT NOT NULL, - tags_json TEXT NOT NULL, - readme_filename TEXT, - readme_content TEXT, - exported_at TEXT NOT NULL, - UNIQUE (repo_full_name, commit_sha) - ) - `); - const getSql = "SELECT * FROM replay_snapshots WHERE repo_full_name = ? AND commit_sha = ?"; - const insertSql = ` - INSERT INTO replay_snapshots - (repo_full_name, commit_sha, worktree_path, target_date, commits_json, tags_json, readme_filename, readme_content, exported_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - function getSnapshot(repoFullName, commitSha) { - const row = driver.query(getSql, [normalizeRepoFullName(repoFullName), normalizeCommitSha(commitSha)]).rows[0]; - return row ? rowToSnapshot(row) : null; - } - function saveSnapshot(snapshot) { - const repoFullName = normalizeRepoFullName(snapshot.repoFullName); - const commitSha = normalizeCommitSha(snapshot.commitSha); - driver.query(insertSql, [ - repoFullName, - commitSha, - snapshot.worktreePath, - snapshot.targetDate, - JSON.stringify(snapshot.commits), - JSON.stringify(snapshot.tags), - snapshot.readme?.filename ?? null, - snapshot.readme?.content ?? null, - new Date().toISOString(), - ]); - // Non-null: the INSERT above either succeeded (this row now exists) or threw, so getSnapshot here always - // finds the row it just wrote. - return getSnapshot(repoFullName, commitSha); - } - return { - dbPath: resolvedPath, - getSnapshot, - saveSnapshot, - close() { - db.close(); - }, - }; -} -function getDefaultReplaySnapshotStore() { - defaultDb ??= openReplaySnapshotStore(); - return defaultDb; -} -export function closeDefaultReplaySnapshotStore() { - if (!defaultDb) - return; - defaultDb.close(); - defaultDb = null; -} -/** - * Export a frozen, reproducible replay snapshot for (repoFullName, commitSha): a detached working-tree checkout - * at that commit plus a context bundle (commit history, reachable tags, README-at-commit). Returns the CACHED - * snapshot without touching git again if one already exists for this exact (repo, commit) pair. - */ -export async function exportReplaySnapshot(input, deps) { - if (!input || typeof input !== "object") - throw new Error("invalid_replay_snapshot_input"); - const repoFullName = normalizeRepoFullName(input.repoFullName); - const commitSha = normalizeCommitSha(input.commitSha); - if (typeof input.repoPath !== "string" || !input.repoPath.trim()) - throw new Error("invalid_repo_path"); - const repoPath = input.repoPath.trim(); - if (!deps || typeof deps !== "object" || typeof deps.exec !== "function") - throw new Error("invalid_exec"); - const { exec } = deps; - const store = deps.store ?? getDefaultReplaySnapshotStore(); - const cached = store.getSnapshot(repoFullName, commitSha); - if (cached) - return cached; - const worktreePath = planReplaySnapshotPath({ repoPath, commitSha }); - await addDetachedWorktree(exec, repoPath, worktreePath, commitSha); - // Everything below can fail (a bad git read, or a deliberate freshness violation) after the worktree already - // exists on disk at the deterministic path above. Left behind, a retry for the same (repo, commit) pair would - // hit `git worktree add`'s own "path already exists" refusal instead of the real error, permanently masking - // it. Clean up the worktree on any failure here before rethrowing, so a retry starts from a clean slate. - try { - const targetDate = await readTargetCommitDate(exec, repoPath, commitSha); - const commits = await readCommitHistory(exec, repoPath, commitSha); - const tags = await readReachableTags(exec, repoPath, commitSha); - const readme = await readReadmeAtCommit(exec, repoPath, commitSha); - validateSnapshotFreshness({ targetDate, commits, tags }); - return store.saveSnapshot({ repoFullName, commitSha, worktreePath, targetDate, commits, tags, readme }); - } - catch (error) { - await removeReplaySnapshotWorktree(exec, repoPath, worktreePath).catch(() => { - /* best-effort cleanup -- the original error below is the one that matters to the caller */ - }); - throw error; - } -} -/** Tear down a replay snapshot's working-tree export (the cached context-bundle row is left in place -- it is - * cheap, commit-keyed, and re-usable even after the on-disk tree is removed; only re-adding the worktree would - * require the tree again, which is out of this function's scope). */ -export async function removeReplaySnapshotWorktree(exec, repoPath, worktreePath) { - return removeWorktree({ exec, repoPath, worktreePath }); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LXNuYXBzaG90LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVwbGF5LXNuYXBzaG90LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDakMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRWxELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSx1QkFBdUIsRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBcUQ3RyxNQUFNLGlCQUFpQixHQUFHLHlCQUF5QixDQUFDO0FBQ3BELElBQUksU0FBUyxHQUErQixJQUFJLENBQUM7QUFFakQsTUFBTSxVQUFVLDJCQUEyQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQy9GLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsbUNBQW1DLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsMkJBQTJCLEVBQUUsRUFBRSxpQ0FBaUMsQ0FBQyxDQUFDO0FBQzdHLENBQUM7QUFFRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUM7QUFDekIsTUFBTSxtQkFBbUIsR0FBRyxtQkFBbUIsQ0FBQztBQUVoRCxTQUFTLHFCQUFxQixDQUFDLFlBQW9CO0lBQ2pELElBQUksT0FBTyxZQUFZLEtBQUssUUFBUTtRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUNoRixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxZQUFZLENBQUMsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQzVELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxNQUFNLElBQUksS0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/replay-task-bridge.d.ts b/packages/loopover-miner/lib/replay-task-bridge.d.ts deleted file mode 100644 index abca7ddf5a..0000000000 --- a/packages/loopover-miner/lib/replay-task-bridge.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ForwardRefContext, FreezePointCandidate, ReplayScoringKey, ReplayScoringKeyRejected, ReplayTask, ReplayTaskOptions, ReplayTaskRejected } from "./replay-task-generation.js"; -export type ReplaySnapshotCommit = { - sha?: string; - date?: string; - subject?: string; -}; -export type ReplaySnapshotTag = { - name?: string; - date?: string; - targetSha?: string; -}; -export type ReplaySnapshot = { - repoFullName?: string; - commitSha?: string; - targetDate?: string; - commits?: ReplaySnapshotCommit[]; - tags?: ReplaySnapshotTag[]; - readme?: { - filename?: string; - content?: string; - } | null; - [key: string]: unknown; -}; -export type RevealedReplaySide = { - revealedCommitCount?: number; - revealedGroundTruth?: unknown; -}; -export type SnapshotIssueContext = { - knownIssueMax?: number; - revealedIssueNumbers?: number[]; -}; -export declare function collectFrozenContextTexts(snapshot: ReplaySnapshot): string[]; -export declare function buildLeakageContextFromSnapshot(snapshot: ReplaySnapshot, issueContext?: SnapshotIssueContext): ForwardRefContext; -export declare function buildReplayCandidateFromSnapshot(snapshot: ReplaySnapshot, revealed?: RevealedReplaySide): FreezePointCandidate; -export declare function generateLeakageSafeReplayTask(snapshot: ReplaySnapshot, revealed?: RevealedReplaySide, issueContext?: SnapshotIssueContext, options?: ReplayTaskOptions): ReplayTask | ReplayTaskRejected; -export declare function generateLeakageSafeScoringKey(snapshot: ReplaySnapshot, revealed?: RevealedReplaySide, options?: ReplayTaskOptions): ReplayScoringKey | ReplayScoringKeyRejected; diff --git a/packages/loopover-miner/lib/replay-task-bridge.js b/packages/loopover-miner/lib/replay-task-bridge.js deleted file mode 100644 index 9e61e59943..0000000000 --- a/packages/loopover-miner/lib/replay-task-bridge.js +++ /dev/null @@ -1,89 +0,0 @@ -// Bridge (#6160): turns a frozen replay snapshot (replay-snapshot.js, #3010) into a leakage-safe replay task via -// the leakage-safe generator (replay-task-generation.js, #3011), so the historical-replay calibration flow -// (calibration-run.js, #4248) never scores a task built from historical context that still leaks post-T state. -// -// #3011 shipped the leakage-safe generator (scrub/lint/select/classify + generateReplayTask) with ZERO callers: -// nothing turned a #3010 snapshot into a task, so scrubForwardReferences/lintFrozenContext were never actually run -// on real historical-replay data -- the safety harness sat unused. This module is that missing seam. It reads the -// snapshot's own free-text context (README-at-T, commit subjects, reachable tag names -- the fields a forward -// reference can hide in), derives the leakage context the snapshot already knows (every pre-T commit SHA it -// carries), and hands both to generateReplayTask, which LINTS then SCRUBS the frozen context BEFORE returning a -// task -- exactly #3011's original design intent. -// -// REMAINING GAP (noted honestly, per the issue): the snapshot is git-only, so it cannot know issue numbers. -// `knownIssueMax` / `revealedIssueNumbers` -- the calibration harness's issue-history knowledge -- must be supplied -// by the caller. And the replay EXECUTOR that turns these frozen tasks into the `{ replayPlan, revealedHistory }` -// results calibration-run.js scores is still unbuilt (nothing calls exportReplaySnapshot yet either). This bridge -// wires the two halves that DO exist -- snapshot -> leakage-safe task -- and leaves that executor as the next -// connective step. Every function here is pure and deterministic (no clock, no randomness, no IO). -import { generateReplayScoringKey, generateReplayTask } from "./replay-task-generation.js"; -function assertSnapshot(snapshot) { - if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) { - throw new Error("invalid_replay_snapshot"); - } - return snapshot; -} -function snapshotCommits(snapshot) { - return Array.isArray(snapshot.commits) ? snapshot.commits : []; -} -// The snapshot's free-text context, in a fixed order: README-at-T, then each commit subject, then each reachable -// tag name. Unlike the structurally pre-T-validated SHAs/dates, these are author-controlled prose where a forward -// reference can hide, so they are exactly what must be scrubbed/linted before a task is frozen. Empty and -// non-string fields are skipped so they never dilute the frozen context. -export function collectFrozenContextTexts(snapshot) { - assertSnapshot(snapshot); - const texts = []; - const readmeContent = snapshot.readme?.content; - if (typeof readmeContent === "string" && readmeContent.length > 0) - texts.push(readmeContent); - for (const commit of snapshotCommits(snapshot)) { - if (typeof commit?.subject === "string" && commit.subject.length > 0) - texts.push(commit.subject); - } - const tags = Array.isArray(snapshot.tags) ? snapshot.tags : []; - for (const tag of tags) { - if (typeof tag?.name === "string" && tag.name.length > 0) - texts.push(tag.name); - } - return texts; -} -// The leakage context #3011's scrubber needs. The pre-T commit SHAs are DERIVED from the snapshot itself (it -// carries the full ancestry up to T), so a snapshot's own commits are never mistaken for forward references; -// the issue-number knowledge the git snapshot cannot hold is passed through from the caller. -export function buildLeakageContextFromSnapshot(snapshot, issueContext = {}) { - assertSnapshot(snapshot); - return { - knownIssueMax: issueContext.knownIssueMax, - knownCommitShas: snapshotCommits(snapshot).map((commit) => commit?.sha), - revealedIssueNumbers: issueContext.revealedIssueNumbers, - }; -} -// The freeze-point candidate #3011's selector/generator expect, mapped from the snapshot plus the revealed post-T -// side (commit count + ground truth) the snapshot deliberately does not hold. -export function buildReplayCandidateFromSnapshot(snapshot, revealed = {}) { - assertSnapshot(snapshot); - return { - repo: typeof snapshot.repoFullName === "string" ? snapshot.repoFullName : null, - commitT: typeof snapshot.commitSha === "string" ? snapshot.commitSha : null, - lastActivityAt: typeof snapshot.targetDate === "string" ? snapshot.targetDate : null, - priorCommitCount: snapshotCommits(snapshot).length, - revealedCommitCount: revealed.revealedCommitCount, - revealedGroundTruth: revealed.revealedGroundTruth, - frozenContextTexts: collectFrozenContextTexts(snapshot), - }; -} -// The wiring. Builds the leakage context + candidate from the snapshot, then runs generateReplayTask, which LINTS -// the frozen context (rejecting on any unscrubbable forward reference) and SCRUBS the surviving text before -// returning the frozen task -- so a replay task is never generated from leaky historical context. -export function generateLeakageSafeReplayTask(snapshot, revealed = {}, issueContext = {}, options = {}) { - const context = buildLeakageContextFromSnapshot(snapshot, issueContext); - const candidate = buildReplayCandidateFromSnapshot(snapshot, revealed); - return generateReplayTask(candidate, context, options); -} -// Scoring-only sibling: the isolated post-execution scorer key for the same snapshot. It shares only selection -// eligibility with the task above and never carries frozen context (mirroring #3011's own generate/scoring split), -// so a caller must check generateLeakageSafeReplayTask's own result before treating the two as a matched pair. -export function generateLeakageSafeScoringKey(snapshot, revealed = {}, options = {}) { - return generateReplayScoringKey(buildReplayCandidateFromSnapshot(snapshot, revealed), options); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LXRhc2stYnJpZGdlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVwbGF5LXRhc2stYnJpZGdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGlIQUFpSDtBQUNqSCwyR0FBMkc7QUFDM0csK0dBQStHO0FBQy9HLEVBQUU7QUFDRixnSEFBZ0g7QUFDaEgsbUhBQW1IO0FBQ25ILGtIQUFrSDtBQUNsSCw4R0FBOEc7QUFDOUcsNEdBQTRHO0FBQzVHLGdIQUFnSDtBQUNoSCxrREFBa0Q7QUFDbEQsRUFBRTtBQUNGLDRHQUE0RztBQUM1RyxvSEFBb0g7QUFDcEgsa0hBQWtIO0FBQ2xILGtIQUFrSDtBQUNsSCw4R0FBOEc7QUFDOUcsbUdBQW1HO0FBRW5HLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBMkMzRixTQUFTLGNBQWMsQ0FBQyxRQUF3QjtJQUM5QyxJQUFJLENBQUMsUUFBUSxJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDekUsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFDRCxPQUFPLFFBQVEsQ0FBQztBQUNsQixDQUFDO0FBRUQsU0FBUyxlQUFlLENBQUMsUUFBd0I7SUFDL0MsT0FBTyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0FBQ2pFLENBQUM7QUFFRCxpSEFBaUg7QUFDakgsa0hBQWtIO0FBQ2xILDBHQUEwRztBQUMxRyx5RUFBeUU7QUFDekUsTUFBTSxVQUFVLHlCQUF5QixDQUFDLFFBQXdCO0lBQ2hFLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN6QixNQUFNLEtBQUssR0FBYSxFQUFFLENBQUM7SUFD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/replay-task-generation.d.ts b/packages/loopover-miner/lib/replay-task-generation.d.ts deleted file mode 100644 index b58de58e93..0000000000 --- a/packages/loopover-miner/lib/replay-task-generation.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -export declare const FORWARD_REF_PLACEHOLDER = "[redacted-forward-ref]"; -export type RecencyPool = "recent" | "older"; -export declare const RECENCY_POOLS: readonly RecencyPool[]; -export type ForwardReference = { - kind: "link" | "hashref" | "sha" | "bare-issue-number"; - value: string | number; -}; -export type ForwardRefContext = { - knownIssueMax?: number; - knownCommitShas?: string[]; - revealedIssueNumbers?: number[]; -}; -export type DetectedForwardReferences = { - scrubbable: ForwardReference[]; - unscrubbable: ForwardReference[]; -}; -export type ScrubResult = { - scrubbed: string; - removed: ForwardReference[]; - residual: ForwardReference[]; -}; -export type LintResult = { - ok: boolean; - residual: ForwardReference[]; -}; -export type FreezePointThresholds = { - minPriorCommits?: number; - minRevealedCommits?: number; -}; -export type FreezePointCandidate = { - repo?: string; - commitT?: string; - priorCommitCount?: number; - revealedCommitCount?: number; - lastActivityAt?: string; - frozenContextTexts?: unknown[]; - revealedGroundTruth?: unknown; -}; -export type FreezePointSelection = { - eligible: boolean; - reasons: string[]; - priorCommitCount: number; - revealedCommitCount: number; -}; -export type ReplayTaskOptions = { - thresholds?: FreezePointThresholds; - modelCutoffIso?: string; -}; -export type ReplayTaskRejected = { - eligible: false; - rejected: "selection" | "unscrubbable_forward_reference"; - reasons?: string[]; - residual?: ForwardReference[]; -}; -export type ReplayTask = { - eligible: true; - pool: RecencyPool; - frozen: { - repo: string | null; - commitT: string | null; - contextTexts: string[]; - }; -}; -export type ReplayScoringKey = { - eligible: true; - commitCount: number; - groundTruth: unknown; -}; -export type ReplayScoringKeyRejected = { - eligible: false; - rejected: "selection"; - reasons: string[]; -}; -export declare function detectForwardReferences(text: unknown, context: ForwardRefContext | null | undefined): DetectedForwardReferences; -export declare function scrubForwardReferences(text: unknown, context: ForwardRefContext | null | undefined): ScrubResult; -export declare function lintFrozenContext(texts: unknown, context: ForwardRefContext | null | undefined): LintResult; -export declare function selectFreezePoint(candidate: FreezePointCandidate | null | undefined, thresholds: FreezePointThresholds | null | undefined): FreezePointSelection; -export declare function classifyRecencyPool(candidate: FreezePointCandidate | null | undefined, options: { - modelCutoffIso?: string; -} | null | undefined): RecencyPool; -export declare function generateReplayTask(candidate: FreezePointCandidate | null | undefined, context: ForwardRefContext | null | undefined, options: ReplayTaskOptions | null | undefined): ReplayTask | ReplayTaskRejected; -export declare function generateReplayScoringKey(candidate: FreezePointCandidate | null | undefined, options: ReplayTaskOptions | null | undefined): ReplayScoringKey | ReplayScoringKeyRejected; diff --git a/packages/loopover-miner/lib/replay-task-generation.js b/packages/loopover-miner/lib/replay-task-generation.js deleted file mode 100644 index 965575a81a..0000000000 --- a/packages/loopover-miner/lib/replay-task-generation.js +++ /dev/null @@ -1,197 +0,0 @@ -// Leakage-safe task generation for the historical-replay calibration harness (#3011). -// -// A frozen snapshot at commit T is only useful for calibration if (a) the freeze point has enough real -// history on both sides to be worth scoring, and (b) nothing in the frozen context lets a replay run infer -// the future by pattern-matching text rather than reasoning. This module selects calibration-worthy freeze -// points, scrubs forward references out of the frozen context, tags each point's recency pool, and returns -// the frozen replay task without the revealed post-T ground truth. Scoring data is exposed through a separate -// function so replay execution never has to hold both sides at once. Every function here is pure and -// deterministic — no clock, no randomness, no IO — so a given (candidate, context) always yields an -// identical task. -// What a scrubbed-away forward reference is replaced with. A fixed, self-delimiting token so the scrubbed -// text stays readable and the substitution is itself deterministic. -export const FORWARD_REF_PLACEHOLDER = "[redacted-forward-ref]"; -// Recency pools. Freeze points are mixed across these bands so a judge/planner that has memorized recent -// public history cannot dominate the calibration signal. -export const RECENCY_POOLS = Object.freeze(["recent", "older"]); -function toIssueNumberSet(values) { - const set = new Set(); - if (Array.isArray(values)) { - for (const value of values) { - if (Number.isInteger(value) && value > 0) - set.add(value); - } - } - return set; -} -function toShaSet(values) { - const set = new Set(); - if (Array.isArray(values)) { - for (const value of values) { - if (typeof value === "string" && /^[0-9a-f]{7,40}$/i.test(value)) - set.add(value.toLowerCase()); - } - } - return set; -} -function resolveContext(context) { - return { - knownIssueMax: Number.isInteger(context?.knownIssueMax) && context?.knownIssueMax >= 0 ? context?.knownIssueMax : 0, - knownCommitShas: toShaSet(context?.knownCommitShas), - revealedIssueNumbers: toIssueNumberSet(context?.revealedIssueNumbers), - }; -} -// Core scanner shared by scrub/detect/lint. Walks a text in a fixed priority order (deep-links first, so an -// issue/PR/commit URL is handled before its inner number/SHA can match a barer pattern) and classifies each -// forward reference as either: -// - scrubbable: a self-delimited token (`#123`, a GitHub issues/pull/commit URL, or a raw commit SHA) that -// resolves only to post-T state and can be safely replaced with the placeholder; or -// - unscrubbable: a *bare* integer that exactly matches a known post-T issue number. A bare number cannot be -// blanket-removed without destroying legitimate pre-T numbers (versions, counts), so it is detected but -// left in place — its presence must fail the freeze point rather than be silently mangled. -function processForwardReferences(rawText, context) { - const resolved = resolveContext(context); - const removed = []; - const text = typeof rawText === "string" ? rawText : ""; - // 1. GitHub issue/pull deep-links whose number is after T. - let scrubbed = text.replace(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+\/(?:issues|pull)\/(\d+)\b/gi, (match, digits) => { - if (Number(digits) > resolved.knownIssueMax) { - removed.push({ kind: "link", value: match }); - return FORWARD_REF_PLACEHOLDER; - } - return match; - }); - // 2. GitHub commit deep-links whose SHA is not in pre-T history. - scrubbed = scrubbed.replace(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+\/commit\/([0-9a-f]{7,40})\b/gi, (match, sha) => { - if (!resolved.knownCommitShas.has(sha.toLowerCase())) { - removed.push({ kind: "link", value: match }); - return FORWARD_REF_PLACEHOLDER; - } - return match; - }); - // 3. Bare `#123` issue/PR references after T (not already inside a now-removed link). - scrubbed = scrubbed.replace(/(^|[^\w/])#(\d+)\b/g, (match, prefix, digits) => { - if (Number(digits) > resolved.knownIssueMax) { - removed.push({ kind: "hashref", value: `#${digits}` }); - return `${prefix}${FORWARD_REF_PLACEHOLDER}`; - } - return match; - }); - // 4. Raw commit SHAs not in pre-T history. Require at least one hex letter so a plain decimal number is - // never misread as a SHA — those flow to the bare-issue-number residual check below instead. - scrubbed = scrubbed.replace(/(^|[^\w/#])([0-9a-f]{7,40})\b/gi, (match, prefix, sha) => { - if (!/[a-f]/i.test(sha)) - return match; - if (!resolved.knownCommitShas.has(sha.toLowerCase())) { - removed.push({ kind: "sha", value: sha }); - return `${prefix}${FORWARD_REF_PLACEHOLDER}`; - } - return match; - }); - // Residual: bare integers that name a real post-T issue and so leak the future, but cannot be safely - // auto-removed. Detected against the surviving text — if any remain, the freeze point is not usable as-is. - const residual = []; - if (resolved.revealedIssueNumbers.size > 0) { - for (const bareMatch of scrubbed.matchAll(/(?:^|[^\w#/])(\d+)\b/g)) { - const value = Number(bareMatch[1]); - if (resolved.revealedIssueNumbers.has(value)) { - residual.push({ kind: "bare-issue-number", value }); - } - } - } - return { scrubbed, removed, residual }; -} -// Detect forward references in text without modifying it, split by whether they can be safely scrubbed. -export function detectForwardReferences(text, context) { - const { removed, residual } = processForwardReferences(text, context); - return { scrubbable: removed, unscrubbable: residual }; -} -// Scrub the safely-removable forward references from text, returning the cleaned text, what was removed, and -// any unscrubbable references that remain (a non-empty `residual` means the text still leaks the future). -export function scrubForwardReferences(text, context) { - return processForwardReferences(text, context); -} -// A freeze point's frozen context is clean iff every provided text scrubs to zero residual forward references. -export function lintFrozenContext(texts, context) { - const list = Array.isArray(texts) ? texts : texts == null ? [] : [texts]; - const residual = []; - for (const text of list) { - residual.push(...processForwardReferences(text, context).residual); - } - return { ok: residual.length === 0, residual }; -} -// Selection: a freeze point is calibration-worthy only with enough real history on both sides of T. -export function selectFreezePoint(candidate, thresholds) { - const minPriorCommits = Number.isInteger(thresholds?.minPriorCommits) ? thresholds?.minPriorCommits : 0; - const minRevealedCommits = Number.isInteger(thresholds?.minRevealedCommits) - ? thresholds?.minRevealedCommits - : 0; - const priorCommitCount = Number.isInteger(candidate?.priorCommitCount) ? candidate?.priorCommitCount : 0; - const revealedCommitCount = Number.isInteger(candidate?.revealedCommitCount) - ? candidate?.revealedCommitCount - : 0; - const reasons = []; - if (priorCommitCount < minPriorCommits) - reasons.push("insufficient_prior_history"); - if (revealedCommitCount < minRevealedCommits) - reasons.push("insufficient_revealed_history"); - return { eligible: reasons.length === 0, reasons, priorCommitCount, revealedCommitCount }; -} -// Pool provenance: a freeze point whose last activity is at/after the calibration run's model cutoff is -// "recent" (higher memorization risk); everything else, including an unknown date, is "older". ISO-8601 -// timestamps sort lexicographically, so no clock is needed. -export function classifyRecencyPool(candidate, options) { - const modelCutoffIso = typeof options?.modelCutoffIso === "string" ? options.modelCutoffIso : ""; - const lastActivityAt = typeof candidate?.lastActivityAt === "string" ? candidate.lastActivityAt : ""; - if (!modelCutoffIso || !lastActivityAt) - return "older"; - return lastActivityAt >= modelCutoffIso ? "recent" : "older"; -} -// One-shot replay generator. Applies selection, then scrubs and lints the frozen context, then returns only -// the frozen replay task. Revealed post-T ground truth is intentionally available only through -// generateReplayScoringKey, so a replay worker/serializer/logger/model call never receives both sides. -export function generateReplayTask(candidate, context, options) { - const selection = selectFreezePoint(candidate, options?.thresholds); - if (!selection.eligible) { - return { eligible: false, rejected: "selection", reasons: selection.reasons }; - } - const frozenTexts = Array.isArray(candidate?.frozenContextTexts) ? candidate.frozenContextTexts : []; - const lint = lintFrozenContext(frozenTexts, context); - if (!lint.ok) { - return { eligible: false, rejected: "unscrubbable_forward_reference", residual: lint.residual }; - } - const pool = classifyRecencyPool(candidate, options); - const scrubbedTexts = frozenTexts.map((text) => processForwardReferences(text, context).scrubbed); - return { - eligible: true, - pool, - frozen: { - repo: typeof candidate?.repo === "string" ? candidate.repo : null, - commitT: typeof candidate?.commitT === "string" ? candidate.commitT : null, - contextTexts: scrubbedTexts, - }, - }; -} -// Scoring-only accessor. Call this from the isolated scorer path after replay execution has finished; do not -// pass its result to replay workers. It deliberately shares only selection eligibility with generateReplayTask -// and never carries frozen context. -// -// IMPORTANT: `eligible: true` here means only that selectFreezePoint accepted the candidate -- it does NOT -// mean generateReplayTask would also produce a usable frozen task for it. generateReplayTask can still reject -// a selection-eligible candidate afterward (`rejected: "unscrubbable_forward_reference"`, from -// lintFrozenContext), because scrub/lint eligibility is about the FROZEN CONTEXT TEXT, which this function -// never touches -- it only reveals commitCount/groundTruth, so lint/scrub has nothing to check here. A caller -// must not assume a scoring key implies a replay task was ever generated for the same candidate; check -// generateReplayTask's own result independently before treating the two as a matched pair. -export function generateReplayScoringKey(candidate, options) { - const selection = selectFreezePoint(candidate, options?.thresholds); - if (!selection.eligible) { - return { eligible: false, rejected: "selection", reasons: selection.reasons }; - } - return { - eligible: true, - commitCount: selection.revealedCommitCount, - groundTruth: candidate?.revealedGroundTruth ?? null, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LXRhc2stZ2VuZXJhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlcGxheS10YXNrLWdlbmVyYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsc0ZBQXNGO0FBQ3RGLEVBQUU7QUFDRix1R0FBdUc7QUFDdkcsMkdBQTJHO0FBQzNHLDJHQUEyRztBQUMzRywyR0FBMkc7QUFDM0csOEdBQThHO0FBQzlHLHFHQUFxRztBQUNyRyxvR0FBb0c7QUFDcEcsa0JBQWtCO0FBRWxCLDBHQUEwRztBQUMxRyxvRUFBb0U7QUFDcEUsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsd0JBQXdCLENBQUM7QUFJaEUseUdBQXlHO0FBQ3pHLHlEQUF5RDtBQUN6RCxNQUFNLENBQUMsTUFBTSxhQUFhLEdBQTJCLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztBQXdGeEYsU0FBUyxnQkFBZ0IsQ0FBQyxNQUFlO0lBQ3ZDLE1BQU0sR0FBRyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFDOUIsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDMUIsS0FBSyxNQUFNLEtBQUssSUFBSSxNQUFNLEVBQUUsQ0FBQztZQUMzQixJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxHQUFHLENBQUM7Z0JBQUUsR0FBRyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUMzRCxDQUFDO0lBQ0gsQ0FBQztJQUNELE9BQU8sR0FBRyxDQUFDO0FBQ2IsQ0FBQztBQUVELFNBQVMsUUFBUSxDQUFDLE1BQWU7SUFDL0IsTUFBTSxHQUFHLEdBQUcsSUFBSSxHQUFHLEVBQVUsQ0FBQztJQUM5QixJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUMxQixLQUFLLE1BQU0sS0FBSyxJQUFJLE1BQU0s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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/repo-clone.d.ts b/packages/loopover-miner/lib/repo-clone.d.ts deleted file mode 100644 index 413460dee7..0000000000 --- a/packages/loopover-miner/lib/repo-clone.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -export type EnsureRepoClonedResult = { - ok: boolean; - repoPath: string; - error?: string; -}; -export type RunGitFn = (args: string[], cwd: string, timeoutMs: number) => Promise<{ - ok: boolean; - stdout: string; - stderr: string; -}>; -export type RepoCloneLockOptions = { - lockTimeoutMs?: number; - lockStaleMs?: number; - lockPollMs?: number; - nowMs?: () => number; - lockSleep?: (ms: number) => Promise; - isProcessAlive?: (pid: number) => boolean; - openLock?: (lockPath: string) => number; - writeLock?: (fd: number, data: string) => void; -}; -type EnsureRepoClonedOptions = { - baseBranch?: string; - cloneBaseDir?: string; - env?: Record; - timeoutMs?: number; - remoteUrl?: string; - runGit?: RunGitFn; -} & RepoCloneLockOptions; -export declare function resolveRepoCloneBaseDir(env?: Record): string; -export declare const REPO_SEGMENT_PATTERN: RegExp; -export declare function isPathTraversalSegment(segment: string): boolean; -export declare function isValidRepoSegment(segment: unknown): boolean; -export declare function resolveRepoCloneDir(repoFullName: string, env?: Record): string; -/** - * Decide whether an existing clone lockfile is stale (reclaimable): true when the file is missing or its JSON is - * unreadable/partial (a crash mid-write), when its owner pid is confirmed dead within the SAME host's namespace, - * or -- ONLY for an owner this host cannot probe (a different host/container, or a malformed record with no - * usable pid) -- when it is older than `staleMs`. A same-host owner whose pid IS probeable is judged purely by - * liveness: a live one is never stale no matter how long its clone legitimately runs (age reclaim there would - * yank the lock out from under an in-progress clone -- a double-holder bug), and a dead one is stale at once. - */ -export declare function isRepoCloneLockStale(lockPath: string, nowMs: number, staleMs: number, isAlive?: (pid: number) => boolean): boolean; -/** - * Take the cross-process clone lock for `repoPath`, returning an idempotent `release()`. Atomically create-and-holds - * `${repoPath}.clone.lock` (open .., 'wx'); on contention it reclaims a stale lock (see {@link isRepoCloneLockStale}) - * or waits `lockPollMs` between retries until `lockTimeoutMs` elapses, then throws `repo_clone_lock_timeout` (fail - * closed). Registered for crash-safe cleanup so a SIGINT/SIGTERM releases it. `nowMs`/`lockSleep`/`isProcessAlive`/ - * `openLock`/`writeLock` are injectable for tests; every real caller relies on the defaults. - */ -export declare function acquireRepoCloneLock(repoPath: string, options?: RepoCloneLockOptions): Promise<() => void>; -/** - * Serialize the git mutations of {@link ensureRepoClonedUnlocked} per resolved repo path so concurrent - * same-repo attempts never race the shared base clone (#6762), while different repos still run in parallel. - * Resolves the same `repoPath` the unlocked step computes and uses it as the mutex key; throws (before - * locking) on a malformed `repoFullName`, matching the prior behaviour. - */ -export declare function ensureRepoCloned(repoFullName: string, options?: EnsureRepoClonedOptions): Promise; -export {}; diff --git a/packages/loopover-miner/lib/repo-clone.js b/packages/loopover-miner/lib/repo-clone.js deleted file mode 100644 index 4121a9a156..0000000000 --- a/packages/loopover-miner/lib/repo-clone.js +++ /dev/null @@ -1,289 +0,0 @@ -import { execFile } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs"; -import { homedir, hostname } from "node:os"; -import { dirname, join } from "node:path"; -import { promisify } from "node:util"; -import { registerCleanupResource } from "./process-lifecycle.js"; -import { isProcessAlive } from "./worktree-allocator.js"; -// Per-repo base-clone cache (#5132, Wave 3.5 follow-up). packages/loopover-engine/src/miner/ -// worktree-allocator.ts's real `addWorktree` primitive (git worktree add -b ) -// requires an EXISTING git clone to branch off -- it has never been wired into this package because that -// clone-management step didn't exist yet. This module is that step: clone a target repo once, then keep it -// current (fetch + hard-reset to the base branch) on every subsequent attempt, so `addWorktree` always -// branches off real, fresh content. Relies entirely on whatever git/gh credentials are already configured -// on this machine -- same assumption execute-local-write.js's `gh pr create` already makes; this module -// never embeds a token in a clone URL. -const execFileAsync = promisify(execFile); -const DEFAULT_CLONE_DIR_NAME = "repos"; -const DEFAULT_BASE_BRANCH = "main"; -export function resolveRepoCloneBaseDir(env) { - const resolvedEnv = env === undefined ? process.env : env; - const explicitPath = typeof resolvedEnv.LOOPOVER_MINER_REPO_CLONE_DIR === "string" ? resolvedEnv.LOOPOVER_MINER_REPO_CLONE_DIR.trim() : ""; - if (explicitPath) - return explicitPath; - const explicitConfigDir = typeof resolvedEnv.LOOPOVER_MINER_CONFIG_DIR === "string" ? resolvedEnv.LOOPOVER_MINER_CONFIG_DIR.trim() : ""; - if (explicitConfigDir) - return join(explicitConfigDir, DEFAULT_CLONE_DIR_NAME); - const configHome = typeof resolvedEnv.XDG_CONFIG_HOME === "string" && resolvedEnv.XDG_CONFIG_HOME.trim() ? resolvedEnv.XDG_CONFIG_HOME.trim() : join(homedir(), ".config"); - return join(configHome, "loopover-miner", DEFAULT_CLONE_DIR_NAME); -} -// GitHub owner/repo names are restricted to alphanumerics, hyphens, underscores, and periods, and are never -// exactly "." or ".." -- both are rejected here so a value like "../foo" can't make resolveRepoCloneDir's -// join(cloneBaseDir, owner, repo) escape the intended clone directory (a real path-traversal finding). -// Exported so every other owner/repo parser in this package (#5831) shares this one definition instead of -// duplicating it (cross-repo-evaluation.js) or skipping it entirely (attempt-cli.js, claim-ledger-cli.js, -// event-ledger-cli.js, claim-ledger.js). -export const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/; -export function isPathTraversalSegment(segment) { - return segment === "." || segment === ".."; -} -export function isValidRepoSegment(segment) { - return typeof segment === "string" && REPO_SEGMENT_PATTERN.test(segment) && !isPathTraversalSegment(segment); -} -// Reject values that git would interpret as options when passed as argv (e.g. `--upload-pack=...`). -function isUnsafeGitArgValue(value) { - return typeof value === "string" && value.startsWith("-"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return { owner, repo, repoFullName: `${owner}/${repo}` }; -} -export function resolveRepoCloneDir(repoFullName, env) { - const target = normalizeRepoFullName(repoFullName); - return join(resolveRepoCloneBaseDir(env), target.owner, target.repo); -} -async function defaultRunGit(args, cwd, timeoutMs) { - try { - const { stdout, stderr } = await execFileAsync("git", args, { cwd, timeout: timeoutMs }); - return { ok: true, stdout: stdout, stderr: stderr }; - } - catch (error) { - const err = error; - const stderr = typeof err?.stderr === "string" ? err.stderr : ""; - return { ok: false, stdout: "", stderr: stderr || (error instanceof Error ? error.message : String(error)) }; - } -} -// Per-repoPath in-process serialization for ensureRepoCloned (#6762). Two attempts for the SAME repo share -// one deterministic base-clone path and mutate it in place (git fetch/checkout/reset --hard); worktree- -// allocator.js only caps the TOTAL active-slot count, never per-repo exclusivity, so without this two -// same-repo attempts can interleave git subprocesses on the same .git dir and corrupt the index/HEAD/refs or -// trip .git/index.lock. `repoCloneLocks` maps a resolved repoPath to the tail of its in-flight promise chain: -// same-repo calls run strictly one after another, while different repoPaths stay fully parallel. The tail -// promise's handlers swallow, so it never rejects -- one failing attempt can neither reject a waiter nor -// wedge the queue -- and the finally drops the entry once the chain drains, keeping the Map bounded. -const repoCloneLocks = new Map(); -/** Run `fn` under the in-process per-`repoPath` mutex (critical section = one ensureRepoClonedUnlocked). */ -async function withRepoCloneLock(repoPath, fn) { - const previous = repoCloneLocks.get(repoPath) ?? Promise.resolve(); - const run = previous.then(() => fn()); - const tail = run.then(() => { }, () => { }); - repoCloneLocks.set(repoPath, tail); - try { - return await run; - } - finally { - if (repoCloneLocks.get(repoPath) === tail) - repoCloneLocks.delete(repoPath); - } -} -// Cross-process serialization for ensureRepoCloned (#7084). The in-process `repoCloneLocks` Map above only -// serializes callers sharing one Node event loop; fleet mode (DEPLOYMENT.md) runs multiple SEPARATE processes -- -// distinct containers, no shared memory -- against one bind-mounted clone volume, so two of them can still -// interleave git subprocesses on the same .git dir and corrupt the index/HEAD/refs. An OS-level exclusive lockfile -// (open(.., 'wx')) on a deterministic path derived from repoPath closes that gap: create-and-hold is atomic across -// processes, so exactly one holder mutates the clone while a loser waits (bounded) or fails closed. The lock -// records owner pid+host+timestamp so a holder that CRASHES mid-clone doesn't wedge the repo forever -- a same-host -// dead-owner or an over-age lock is reclaimed (mirroring worktree-allocator.js's stale reclaim), and -// registerCleanupResource unlinks it on SIGINT/SIGTERM like this package's other crash-safe resources (#4826). -const DEFAULT_LOCK_TIMEOUT_MS = 10 * 60 * 1000; // comfortably past a slow clone/fetch sequence -const DEFAULT_LOCK_STALE_MS = 15 * 60 * 1000; // a lock older than this is presumed crashed -const DEFAULT_LOCK_POLL_MS = 100; -const defaultLockSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -function repoCloneLockPath(repoPath) { - return `${repoPath}.clone.lock`; -} -/** - * Decide whether an existing clone lockfile is stale (reclaimable): true when the file is missing or its JSON is - * unreadable/partial (a crash mid-write), when its owner pid is confirmed dead within the SAME host's namespace, - * or -- ONLY for an owner this host cannot probe (a different host/container, or a malformed record with no - * usable pid) -- when it is older than `staleMs`. A same-host owner whose pid IS probeable is judged purely by - * liveness: a live one is never stale no matter how long its clone legitimately runs (age reclaim there would - * yank the lock out from under an in-progress clone -- a double-holder bug), and a dead one is stale at once. - */ -export function isRepoCloneLockStale(lockPath, nowMs, staleMs, isAlive = isProcessAlive) { - let meta; - try { - meta = JSON.parse(readFileSync(lockPath, "utf8")); - } - catch { - return true; - } - if (!meta || typeof meta !== "object") - return true; - const record = meta; - // Owner we can directly probe (same host, usable pid): trust liveness exclusively -- alive => held (never - // age-reclaim a still-running local clone), dead => reclaim now. The age backstop below is reserved for an - // owner whose liveness is genuinely unknowable from here. - if (record.host === hostname() && Number.isInteger(record.pid)) { - return !isAlive(record.pid); - } - const atMs = Date.parse(record.at); - if (!Number.isFinite(atMs)) - return true; - return nowMs - atMs > staleMs; -} -/** - * Take the cross-process clone lock for `repoPath`, returning an idempotent `release()`. Atomically create-and-holds - * `${repoPath}.clone.lock` (open .., 'wx'); on contention it reclaims a stale lock (see {@link isRepoCloneLockStale}) - * or waits `lockPollMs` between retries until `lockTimeoutMs` elapses, then throws `repo_clone_lock_timeout` (fail - * closed). Registered for crash-safe cleanup so a SIGINT/SIGTERM releases it. `nowMs`/`lockSleep`/`isProcessAlive`/ - * `openLock`/`writeLock` are injectable for tests; every real caller relies on the defaults. - */ -export async function acquireRepoCloneLock(repoPath, options = {}) { - const lockPath = repoCloneLockPath(repoPath); - const timeoutMs = Number.isFinite(options.lockTimeoutMs) ? options.lockTimeoutMs : DEFAULT_LOCK_TIMEOUT_MS; - const staleMs = Number.isFinite(options.lockStaleMs) ? options.lockStaleMs : DEFAULT_LOCK_STALE_MS; - const pollMs = Number.isFinite(options.lockPollMs) ? options.lockPollMs : DEFAULT_LOCK_POLL_MS; - const now = typeof options.nowMs === "function" ? options.nowMs : Date.now; - const sleep = typeof options.lockSleep === "function" ? options.lockSleep : defaultLockSleep; - const isAlive = typeof options.isProcessAlive === "function" ? options.isProcessAlive : isProcessAlive; - const openLock = typeof options.openLock === "function" ? options.openLock : (path) => openSync(path, "wx", 0o600); - const writeLock = typeof options.writeLock === "function" ? options.writeLock : (fd, data) => writeSync(fd, data); - mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 }); - const deadline = now() + timeoutMs; - for (;;) { - let fd; - try { - fd = openLock(lockPath); - } - catch (error) { - const err = error; - if (!err || err.code !== "EEXIST") - throw error; - if (isRepoCloneLockStale(lockPath, now(), staleMs, isAlive)) { - try { - unlinkSync(lockPath); - } - catch { - // Another waiter reclaimed it first -- just retry the open. - } - continue; - } - if (now() >= deadline) - throw new Error("repo_clone_lock_timeout"); - await sleep(pollMs); - continue; - } - // A per-acquire token stamps THIS holder's ownership so release() can prove the on-disk lock is still ours - // before removing it -- if a peer reclaimed us as stale and re-acquired, the file now carries their token and - // we must not delete their lock (that would let a third caller double-hold). - const token = randomUUID(); - try { - writeLock(fd, JSON.stringify({ pid: process.pid, host: hostname(), at: new Date(now()).toISOString(), token })); - } - catch (error) { - closeSync(fd); - try { - unlinkSync(lockPath); - } - catch { - // best-effort cleanup of our own just-created lock - } - throw error; - } - let released = false; - let unregister = () => { }; - const release = () => { - if (released) - return; - released = true; - unregister(); - try { - closeSync(fd); - } - catch { - // fd already closed - } - try { - // Only remove the lockfile while it still carries OUR token; if a peer reclaimed + re-acquired it, leave - // their lock intact. A missing/unreadable file just means our lock is already gone -- nothing to do. - const current = JSON.parse(readFileSync(lockPath, "utf8")); - if (current && current.token === token) - unlinkSync(lockPath); - } - catch { - // lock already removed or unreadable -- nothing of ours to clean up - } - }; - unregister = registerCleanupResource(release); - return release; - } -} -async function withRepoCloneCrossProcessLock(repoPath, options, fn) { - const release = await acquireRepoCloneLock(repoPath, options); - try { - return await fn(); - } - finally { - release(); - } -} -/** - * Serialize the git mutations of {@link ensureRepoClonedUnlocked} per resolved repo path so concurrent - * same-repo attempts never race the shared base clone (#6762), while different repos still run in parallel. - * Resolves the same `repoPath` the unlocked step computes and uses it as the mutex key; throws (before - * locking) on a malformed `repoFullName`, matching the prior behaviour. - */ -export async function ensureRepoCloned(repoFullName, options = {}) { - const target = normalizeRepoFullName(repoFullName); - const cloneBaseDir = typeof options.cloneBaseDir === "string" && options.cloneBaseDir.trim() ? options.cloneBaseDir.trim() : resolveRepoCloneBaseDir(options.env); - const repoPath = join(cloneBaseDir, target.owner, target.repo); - // Two nested locks: the in-process Map (#6762) keeps same-process callers cheap and ordered, and the - // cross-process lockfile (#7084) additionally serializes separate OS processes sharing the clone volume. - return withRepoCloneLock(repoPath, () => withRepoCloneCrossProcessLock(repoPath, options, () => ensureRepoClonedUnlocked(repoFullName, options))); -} -/** - * Ensure a real, current local clone of `repoFullName` exists at the deterministic per-repo cache path. - * First use: `git clone`. Subsequent use: `git fetch origin` + hard-reset the base branch to - * `origin/`, so every attempt branches off fresh content, not a stale prior checkout. - */ -async function ensureRepoClonedUnlocked(repoFullName, options = {}) { - const target = normalizeRepoFullName(repoFullName); - const baseBranch = typeof options.baseBranch === "string" && options.baseBranch.trim() ? options.baseBranch.trim() : DEFAULT_BASE_BRANCH; - const cloneBaseDir = typeof options.cloneBaseDir === "string" && options.cloneBaseDir.trim() ? options.cloneBaseDir.trim() : resolveRepoCloneBaseDir(options.env); - const repoPath = join(cloneBaseDir, target.owner, target.repo); - const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 120_000; - const runGit = options.runGit ?? defaultRunGit; - if (isUnsafeGitArgValue(baseBranch)) { - return { ok: false, repoPath, error: "invalid_base_branch" }; - } - if (!existsSync(repoPath)) { - mkdirSync(join(cloneBaseDir, target.owner), { recursive: true, mode: 0o700 }); - const cloneUrl = typeof options.remoteUrl === "string" && options.remoteUrl.trim() ? options.remoteUrl.trim() : `https://github.com/${target.owner}/${target.repo}.git`; - if (isUnsafeGitArgValue(cloneUrl)) { - return { ok: false, repoPath, error: "invalid_remote_url" }; - } - const cloned = await runGit(["clone", cloneUrl, repoPath], cloneBaseDir, timeoutMs); - if (!cloned.ok) - return { ok: false, repoPath, error: cloned.stderr || "git_clone_failed" }; - return { ok: true, repoPath }; - } - const fetched = await runGit(["fetch", "origin"], repoPath, timeoutMs); - if (!fetched.ok) - return { ok: false, repoPath, error: fetched.stderr || "git_fetch_failed" }; - const checkedOut = await runGit(["checkout", baseBranch], repoPath, timeoutMs); - if (!checkedOut.ok) - return { ok: false, repoPath, error: checkedOut.stderr || "git_checkout_failed" }; - const reset = await runGit(["reset", "--hard", `origin/${baseBranch}`], repoPath, timeoutMs); - if (!reset.ok) - return { ok: false, repoPath, error: reset.stderr || "git_reset_failed" }; - return { ok: true, repoPath }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwby1jbG9uZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlcG8tY2xvbmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzlDLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDekMsT0FBTyxFQUFFLFNBQVMsRUFBRSxVQUFVLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxZQUFZLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMxRyxPQUFPLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM1QyxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ3RDLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUV6RCw2RkFBNkY7QUFDN0YsMEdBQTBHO0FBQzFHLHlHQUF5RztBQUN6RywyR0FBMkc7QUFDM0csdUdBQXVHO0FBQ3ZHLDBHQUEwRztBQUMxRyx3R0FBd0c7QUFDeEcsdUNBQXVDO0FBRXZDLE1BQU0sYUFBYSxHQUFHLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUMxQyxNQUFNLHNCQUFzQixHQUFHLE9BQU8sQ0FBQztBQUN2QyxNQUFNLG1CQUFtQixHQUFHLE1BQU0sQ0FBQztBQWlDbkMsTUFBTSxVQUFVLHVCQUF1QixDQUFDLEdBQXdDO0lBQzlFLE1BQU0sV0FBVyxHQUFHLEdBQUcsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztJQUMxRCxNQUFNLFlBQVksR0FBRyxPQUFPLFdBQVcsQ0FBQyw2QkFBNkIsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyw2QkFBNkIsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQzNJLElBQUksWUFBWTtRQUFFLE9BQU8sWUFBWSxDQUFDO0lBRXRDLE1BQU0saUJBQWlCLEdBQUcsT0FBTyxXQUFXLENBQUMseUJBQXlCLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMseUJBQXlCLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUN4SSxJQUFJLGlCQUFpQjtRQUFFLE9BQU8sSUFBSSxDQUFDLGlCQUFpQixFQUFFLHNCQUFzQixDQUFDLENBQUM7SUFFOUUsTUFBTSxVQUFVLEdBQUcsT0FBTyxXQUFXLENBQUMsZUFBZSxLQUFLLFFBQVEsSUFBSSxXQUFXLENBQUMsZUFBZSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsZUFBZSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDM0ssT0FBTyxJQUFJLENBQUMsVUFBVSxFQUFFLGdCQUFnQixFQUFFLHNCQUFzQixDQUFDLENBQUM7QUFDcEUsQ0FBQztBQUVELDRHQUE0RztBQUM1RywwR0FBMEc7QUFDMUcsdUdBQXVHO0FBQ3ZHLDBHQUEwRztBQUMxRywwR0FBMEc7QUFDMUcseUNBQXlDO0FBQ3pDLE1BQU0sQ0FBQyxNQUFNLG9CQUFvQixHQUFHLG1CQUFtQixDQUFDO0FBRXhELE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxPQUFlO0lBQ3BELE9BQU8sT0FBTyxLQUFLLEdBQUcsSUFBSSxPQUFPLEtBQUssSUFBSSxDQUFDO0FBQzdDLENBQUM7QUFFRCxNQUFNLFVBQVUsa0JBQWtCLENBQUMsT0FBZ0I7SUFDakQsT0FBTyxPQUFPLE9BQU8sS0FBSyxRQUFRLElBQUksb0JBQW9CLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsc0JBQXNCLENBQUMsT0FBTyxDQUFDLENBQUM7QUFDL0csQ0FBQztBQUVELG9HQUFvRztBQUNwRyxTQUFTLG1CQUFtQixDQUFDLEtBQWM7SUFDekMsT0FBTyxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUM1RCxDQUFDO0FBRUQsU0FBUyxxQkFBcUIsQ0FBQyxZQUFxQjtJQUNsRCxJQUFJLE9BQU8sWUFBWSxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDaEYsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUM1RCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ3RGLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLGtCQUFrQixDQUFDLElBQUksQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUN2RyxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsR0FBRyxLQUFLLElBQUksSUFBSSxFQUFFLEVBQUUsQ0FBQztBQUMzRCxDQUFDO0FBRUQsTUFBTSxVQUFVLG1CQUFtQixDQUFDLFlBQW9CLEVBQUUsR0FBd0M7SUFDaEcsTUFBTSxNQUFNLEdBQUcscUJBQXFCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDbkQsT0FBTyxJQUFJLENBQUMsdUJBQXVCLENBQUMsR0FBRyxDQUFDLEVBQUUsTUFBTSxDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDdkUsQ0FBQztBQUVELEtBQUssVUFBVSxhQUFhLENBQUMsSUFBYyxFQUFFLEdBQVcsRUFBRSxTQUFpQjtJQUN6RSxJQUFJLENBQUM7UUFDSCxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sYUFBYSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsRUFBRSxHQUFHLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFDekYsT0FBTyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLE1BQWdCLEVBQUUsTUFBTSxFQUFFLE1BQWdCLEVBQUUsQ0FBQztJQUMxRSxDQUFDO0lBQUMsT0FBTyxLQUFjLEVBQUUsQ0FBQztRQUN4QixNQUFNLEdBQUcsR0FBRyxLQUFnRCxDQUFDO1FBQzdELE1BQU0sTUFBTSxHQUFHLE9BQU8sR0FBRyxFQUFFLE1BQU0sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNqRSxPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxNQUFNLElBQUksQ0FBQyxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQy9HLENBQUM7QUFDSCxDQUFDO0FBRUQsMkdBQTJHO0FBQzNHLHdHQUF3RztBQUN4RyxzR0FBc0c7QUFDdEcsNkdBQTZHO0FBQzdHLDhHQUE4RztBQUM5RywwR0FBMEc7QUFDMUcseUdBQXlHO0FBQ3pHLHFHQUFxRztBQUNyRyxNQUFNLGNBQWMsR0FBRyxJQUFJLEdBQUcsRUFBeUIsQ0FBQztBQUV4RCw0R0FBNEc7QUFDNUcsS0FBSyxVQUFVLGlCQUFpQixDQUFJLFFBQWdCLEVBQUUsRUFBb0I7SUFDeEUsTUFBTSxRQUFRLEdBQUcsY0FBYyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsSUFBSSxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDbkUsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQ3RDLE1BQU0sSUFBSSxHQUFHLEdBQUcsQ0FBQyxJQUFJLENBQ25CLEdBQUcsRUFBRSxHQUFFLENBQUMsRUFDUixHQUFHLEVBQUUsR0FBRSxDQUFDLENBQ1QsQ0FBQztJQUNGLGNBQWMsQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDO0lBQ25DLElBQUksQ0FBQztRQUNILE9BQU8sTUFBTSxHQUFHLENBQUM7SUFDbkIsQ0FBQztZQUFTLENBQUM7UUFDVCxJQUFJLGNBQWMsQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLEtBQUssSUFBSTtZQUFFLGNBQWMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0UsQ0FBQztBQUNILENBQUM7QUFFRCwyR0FBMkc7QUFDM0csaUhBQWlIO0FBQ2pILDJHQUEyRztBQUMzRyxtSEFBbUg7QUFDbkgsbUhBQW1IO0FBQ25ILDZHQUE2RztBQUM3RyxvSEFBb0g7QUFDcEgscUdBQXFHO0FBQ3JHLCtHQUErRztBQUUvRyxNQUFNLHVCQUF1QixHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUMsK0NBQStDO0FBQy9GLE1BQU0scUJBQXFCLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQyw2Q0FBNkM7QUFDM0YsTUFBTSxvQkFBb0IsR0FBRyxHQUFHLENBQUM7QUFDakMsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLEVBQVUsRUFBaUIsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFFMUcsU0FBUyxpQkFBaUIsQ0FBQyxRQUFnQjtJQUN6QyxPQUFPLEdBQUcsUUFBUSxhQUFhLENBQUM7QUFDbEMsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQ2xDLFFBQWdCLEVBQ2hCLEtBQWEsRUFDYixPQUFlLEVBQ2YsVUFBb0MsY0FBYztJQUVsRCxJQUFJLElBQWEsQ0FBQztJQUNsQixJQUFJLENBQUM7UUFDSCxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxZQUFZLENBQUMsUUFBUSxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDcEQsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUNELElBQUksQ0FBQyxJQUFJLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUTtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ25ELE1BQU0sTUFBTSxHQUFHLElBQXlCLENBQUM7SUFDekMsMEdBQTBHO0lBQzFHLDJHQUEyRztJQUMzRywwREFBMEQ7SUFDMUQsSUFBSSxNQUFNLENBQUMsSUFBSSxLQUFLLFFBQVEsRUFBRSxJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDL0QsT0FBTyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsR0FBYSxDQUFDLENBQUM7SUFDeEMsQ0FBQztJQUNELE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLEVBQVksQ0FBQyxDQUFDO0lBQzdDLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3hDLE9BQU8sS0FBSyxHQUFHLElBQUksR0FBRyxPQUFPLENBQUM7QUFDaEMsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsb0JBQW9CLENBQUMsUUFBZ0IsRUFBRSxVQUFnQyxFQUFFO0lBQzdGLE1BQU0sUUFBUSxHQUFHLGlCQUFpQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzdDLE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsYUFBdUIsQ0FBQyxDQUFDLENBQUMsdUJBQXVCLENBQUM7SUFDckgsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxXQUFxQixDQUFDLENBQUMsQ0FBQyxxQkFBcUIsQ0FBQztJQUM3RyxNQUFNLE1BQU0sR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQW9CLENBQUMsQ0FBQyxDQUFDLG9CQUFvQixDQUFDO0lBQ3pHLE1BQU0sR0FBRyxHQUFHLE9BQU8sT0FBTyxDQUFDLEtBQUssS0FBSyxVQUFVLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7SUFDM0UsTUFBTSxLQUFLLEdBQUcsT0FBTyxPQUFPLENBQUMsU0FBUyxLQUFLLFVBQVUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCLENBQUM7SUFDN0YsTUFBTSxPQUFPLEdBQUcsT0FBTyxPQUFPLENBQUMsY0FBYyxLQUFLLFVBQVUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsY0FBYyxDQUFDO0lBQ3ZHLE1BQU0sUUFBUSxHQUFHLE9BQU8sT0FBTyxDQUFDLFFBQVEsS0FBSyxVQUFVLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBWSxFQUFFLEVBQUUsQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztJQUMzSCxNQUFNLFNBQVMsR0FBRyxPQUFPLE9BQU8sQ0FBQyxTQUFTLEtBQUssVUFBVSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQVUsRUFBRSxJQUFZLEVBQUUsRUFBRSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUUsSUFBSSxDQUFDLENBQUM7SUFFbEksU0FBUyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDL0QsTUFBTSxRQUFRLEdBQUcsR0FBRyxFQUFFLEdBQUcsU0FBUyxDQUFDO0lBQ25DLFNBQVMsQ0FBQztRQUNSLElBQUksRUFBVSxDQUFDO1FBQ2YsSUFBSSxDQUFDO1lBQ0gsRUFBRSxHQUFHLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUMxQixDQUFDO1FBQUMsT0FBTyxLQUFjLEVBQUUsQ0FBQztZQUN4QixNQUFNLEdBQUcsR0FBRyxLQUFpRCxDQUFDO1lBQzlELElBQUksQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLElBQUksS0FBSyxRQUFRO2dCQUFFLE1BQU0sS0FBSyxDQUFDO1lBQy9DLElBQUksb0JBQW9CLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRSxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQUMsRUFBRSxDQUFDO2dCQUM1RCxJQUFJLENBQUM7b0JBQ0gsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDO2dCQUN2QixDQUFDO2dCQUFDLE1BQU0sQ0FBQztvQkFDUCw0REFBNEQ7Z0JBQzlELENBQUM7Z0JBQ0QsU0FBUztZQUNYLENBQUM7WUFDRCxJQUFJLEdBQUcsRUFBRSxJQUFJLFFBQVE7Z0JBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO1lBQ2xFLE1BQU0sS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ3BCLFNBQVM7UUFDWCxDQUFDO1FBQ0QsMkdBQTJHO1FBQzNHLDhHQUE4RztRQUM5Ryw2RUFBNkU7UUFDN0UsTUFBTSxLQUFLLEdBQUcsVUFBVSxFQUFFLENBQUM7UUFDM0IsSUFBSSxDQUFDO1lBQ0gsU0FBUyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsR0FBRyxFQUFFLE9BQU8sQ0FBQyxHQUFHLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxFQUFFLEVBQUUsRUFBRSxJQUFJLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQztRQUNsSCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLFNBQVMsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUNkLElBQUksQ0FBQztnQkFDSCxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDdkIsQ0FBQztZQUFDLE1BQU0sQ0FBQztnQkFDUCxtREFBbUQ7WUFDckQsQ0FBQztZQUNELE1BQU0sS0FBSyxDQUFDO1FBQ2QsQ0FBQztRQUNELElBQUksUUFBUSxHQUFHLEtBQUssQ0FBQztRQUNyQixJQUFJLFVBQVUsR0FBRyxHQUFTLEVBQUUsR0FBRSxDQUFDLENBQUM7UUFDaEMsTUFBTSxPQUFPLEdBQUcsR0FBUyxFQUFFO1lBQ3pCLElBQUksUUFBUTtnQkFBRSxPQUFPO1lBQ3JCLFFBQVEsR0FBRyxJQUFJLENBQUM7WUFDaEIsVUFBVSxFQUFFLENBQUM7WUFDYixJQUFJLENBQUM7Z0JBQ0gsU0FBUyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQ2hCLENBQUM7WUFBQyxNQUFNLENBQUM7Z0JBQ1Asb0JBQW9CO1lBQ3RCLENBQUM7WUFDRCxJQUFJLENBQUM7Z0JBQ0gseUdBQXlHO2dCQUN6RyxxR0FBcUc7Z0JBQ3JHLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLFFBQVEsRUFBRSxNQUFNLENBQUMsQ0FBc0IsQ0FBQztnQkFDaEYsSUFBSSxPQUFPLElBQUksT0FBTyxDQUFDLEtBQUssS0FBSyxLQUFLO29CQUFFLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUMvRCxDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNQLG9FQUFvRTtZQUN0RSxDQUFDO1FBQ0gsQ0FBQyxDQUFDO1FBQ0YsVUFBVSxHQUFHLHVCQUF1QixDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzlDLE9BQU8sT0FBTyxDQUFDO0lBQ2pCLENBQUM7QUFDSCxDQUFDO0FBRUQsS0FBSyxVQUFVLDZCQUE2QixDQUMxQyxRQUFnQixFQUNoQixPQUE2QixFQUM3QixFQUFvQjtJQUVwQixNQUFNLE9BQU8sR0FBRyxNQUFNLG9CQUFvQixDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQztJQUM5RCxJQUFJLENBQUM7UUFDSCxPQUFPLE1BQU0sRUFBRSxFQUFFLENBQUM7SUFDcEIsQ0FBQztZQUFTLENBQUM7UUFDVCxPQUFPLEVBQUUsQ0FBQztJQUNaLENBQUM7QUFDSCxDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGdCQUFnQixDQUNwQyxZQUFvQixFQUNwQixVQUFtQyxFQUFFO0lBRXJDLE1BQU0sTUFBTSxHQUFHLHFCQUFxQixDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQ25ELE1BQU0sWUFBWSxHQUFHLE9BQU8sT0FBTyxDQUFDLFlBQVksS0FBSyxRQUFRLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsdUJBQXVCLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2xLLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxZQUFZLEVBQUUsTUFBTSxDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDL0QscUdBQXFHO0lBQ3JHLHlHQUF5RztJQUN6RyxPQUFPLGlCQUFpQixDQUFDLFFBQVEsRUFBRSxHQUFHLEVBQUUsQ0FDdEMsNkJBQTZCLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyx3QkFBd0IsQ0FBQyxZQUFZLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FDeEcsQ0FBQztBQUNKLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsS0FBSyxVQUFVLHdCQUF3QixDQUNyQyxZQUFvQixFQUNwQixVQUFtQyxFQUFFO0lBRXJDLE1BQU0sTUFBTSxHQUFHLHFCQUFxQixDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQ25ELE1BQU0sVUFBVSxHQUFHLE9BQU8sT0FBTyxDQUFDLFVBQVUsS0FBSyxRQUFRLElBQUksT0FBTyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsbUJBQW1CLENBQUM7SUFDekksTUFBTSxZQUFZLEdBQUcsT0FBTyxPQUFPLENBQUMsWUFBWSxLQUFLLFFBQVEsSUFBSSxPQUFPLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyx1QkFBdUIsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDbEssTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLFlBQVksRUFBRSxNQUFNLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMvRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFNBQW1CLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQztJQUM3RixNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxJQUFJLGFBQWEsQ0FBQztJQUUvQyxJQUFJLG1CQUFtQixDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDcEMsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxxQkFBcUIsRUFBRSxDQUFDO0lBQy9ELENBQUM7SUFFRCxJQUFJLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDMUIsU0FBUyxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUM5RSxNQUFNLFFBQVEsR0FBRyxPQUFPLE9BQU8sQ0FBQyxTQUFTLEtBQUssUUFBUSxJQUFJLE9BQU8sQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLHNCQUFzQixNQUFNLENBQUMsS0FBSyxJQUFJLE1BQU0sQ0FBQyxJQUFJLE1BQU0sQ0FBQztRQUN4SyxJQUFJLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7WUFDbEMsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxvQkFBb0IsRUFBRSxDQUFDO1FBQzlELENBQUM7UUFDRCxNQUFNLE1BQU0sR0FBRyxNQUFNLE1BQU0sQ0FBQyxDQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUUsUUFBUSxDQUFDLEVBQUUsWUFBWSxFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBQ3BGLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRTtZQUFFLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLE1BQU0sSUFBSSxrQkFBa0IsRUFBRSxDQUFDO1FBQzNGLE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDO0lBQ2hDLENBQUM7SUFFRCxNQUFNLE9BQU8sR0FBRyxNQUFNLE1BQU0sQ0FBQyxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsRUFBRSxRQUFRLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDdkUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxFQUFFO1FBQUUsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxPQUFPLENBQUMsTUFBTSxJQUFJLGtCQUFrQixFQUFFLENBQUM7SUFFN0YsTUFBTSxVQUFVLEdBQUcsTUFBTSxNQUFNLENBQUMsQ0FBQyxVQUFVLEVBQUUsVUFBVSxDQUFDLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQy9FLElBQUksQ0FBQyxVQUFVLENBQUMsRUFBRTtRQUFFLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsVUFBVSxDQUFDLE1BQU0sSUFBSSxxQkFBcUIsRUFBRSxDQUFDO0lBRXRHLE1BQU0sS0FBSyxHQUFHLE1BQU0sTUFBTSxDQUFDLENBQUMsT0FBTyxFQUFFLFFBQVEsRUFBRSxVQUFVLFVBQVUsRUFBRSxDQUFDLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQzdGLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRTtRQUFFLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsS0FBSyxDQUFDLE1BQU0sSUFBSSxrQkFBa0IsRUFBRSxDQUFDO0lBRXpGLE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDO0FBQ2hDLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/run-state-cli.d.ts b/packages/loopover-miner/lib/run-state-cli.d.ts deleted file mode 100644 index affc5260bf..0000000000 --- a/packages/loopover-miner/lib/run-state-cli.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { RunState } from "./run-state.js"; -export type ParsedStateGetArgs = { - repoFullName: string; - json: boolean; - apiBaseUrl: string | undefined; -} | { - error: string; -}; -export type ParsedStateSetArgs = { - repoFullName: string; - state: RunState; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; -} | { - error: string; -}; -export declare function parseStateGetArgs(args: string[]): ParsedStateGetArgs; -export declare function parseStateSetArgs(args: string[]): ParsedStateSetArgs; -export declare function runStateGet(args: string[]): number; -export declare function runStateSet(args: string[]): number; -export declare function runStateCli(subcommand: string | undefined, args: string[]): number; diff --git a/packages/loopover-miner/lib/run-state-cli.js b/packages/loopover-miner/lib/run-state-cli.js deleted file mode 100644 index ae91ef51e5..0000000000 --- a/packages/loopover-miner/lib/run-state-cli.js +++ /dev/null @@ -1,148 +0,0 @@ -import { RUN_STATES, getRunState, setRunState } from "./run-state.js"; -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -const STATE_GET_USAGE = "Usage: loopover-miner state get [--api-base-url ] [--json]"; -const STATE_SET_USAGE = "Usage: loopover-miner state set [--api-base-url ] [--dry-run] [--json]"; -const allowedRunStates = new Set(RUN_STATES); -function parseRepoArg(value, usage) { - if (!value) - return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; -} -export function parseStateGetArgs(args) { - const options = { json: false, apiBaseUrl: undefined }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #5563: scope the lookup to a non-default forge host, so it doesn't collide with (or get confused for) a - // same-named repo on the default github.com host. - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: STATE_GET_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 1) { - return { error: STATE_GET_USAGE }; - } - const repo = parseRepoArg(positional[0], STATE_GET_USAGE); - if ("error" in repo) - return repo; - return { repoFullName: repo.repoFullName, ...options }; -} -export function parseStateSetArgs(args) { - const options = { - json: false, - dryRun: false, - apiBaseUrl: undefined, - }; - const positional = []; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what a real state set would do and returns before writing to the run-state store. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: STATE_SET_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - if (positional.length !== 2) { - return { error: STATE_SET_USAGE }; - } - const repo = parseRepoArg(positional[0], STATE_SET_USAGE); - if ("error" in repo) - return repo; - const state = positional[1]; - if (!allowedRunStates.has(state)) { - return { error: `Invalid state: ${state}. Expected one of ${RUN_STATES.join(", ")}.` }; - } - return { repoFullName: repo.repoFullName, state: state, ...options }; -} -export function runStateGet(args) { - const parsed = parseStateGetArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - try { - const state = getRunState(parsed.repoFullName, parsed.apiBaseUrl); - if (parsed.json) { - console.log(JSON.stringify({ repoFullName: parsed.repoFullName, state })); - } - else { - console.log(state ?? "none"); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runStateSet(args) { - const parsed = parseStateSetArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, state: parsed.state }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult)); - } - else { - console.log(`DRY RUN: would set ${parsed.repoFullName}'s run state to "${parsed.state}". No run-state write was made.`); - } - return 0; - } - try { - const write = setRunState(parsed.repoFullName, parsed.state, parsed.apiBaseUrl); - if (parsed.json) { - console.log(JSON.stringify(write)); - } - else { - console.log(write.state); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export function runStateCli(subcommand, args) { - if (subcommand === "get") - return runStateGet(args); - if (subcommand === "set") - return runStateSet(args); - return reportCliFailure(argsWantJson(args), `Unknown state subcommand: ${subcommand ?? ""}. ${STATE_GET_USAGE}`); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVuLXN0YXRlLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJ1bi1zdGF0ZS1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFdEUsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWxGLE1BQU0sZUFBZSxHQUFHLDhFQUE4RSxDQUFDO0FBQ3ZHLE1BQU0sZUFBZSxHQUNuQixnSUFBZ0ksQ0FBQztBQUVuSSxNQUFNLGdCQUFnQixHQUFnQixJQUFJLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQztBQXNCMUQsU0FBUyxZQUFZLENBQUMsS0FBeUIsRUFBRSxLQUFhO0lBQzVELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsQ0FBQztJQUNwQyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDN0IsTUFBTSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNoRCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsSUFBSSxJQUFJLEtBQUssS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUMzQyxPQUFPLEVBQUUsS0FBSyxFQUFFLHdDQUF3QyxFQUFFLENBQUM7SUFDN0QsQ0FBQztJQUNELE9BQU8sRUFBRSxZQUFZLEVBQUUsR0FBRyxLQUFLLElBQUksSUFBSSxFQUFFLEVBQUUsQ0FBQztBQUM5QyxDQUFDO0FBRUQsTUFBTSxVQUFVLGlCQUFpQixDQUFDLElBQWM7SUFDOUMsTUFBTSxPQUFPLEdBQXNELEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsU0FBUyxFQUFFLENBQUM7SUFDMUcsTUFBTSxVQUFV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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/run-state.d.ts b/packages/loopover-miner/lib/run-state.d.ts deleted file mode 100644 index 52eb2167e6..0000000000 --- a/packages/loopover-miner/lib/run-state.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -export type RunState = "idle" | "discovering" | "planning" | "preparing"; -export type RunStateWrite = { - apiBaseUrl: string; - repoFullName: string; - state: RunState; - updatedAt: string; -}; -export type RunStateRow = { - apiBaseUrl: string; - repoFullName: string; - state: RunState; - updatedAt: string; -}; -export type RunStateStore = { - dbPath: string; - getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null; - setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite; - listRunStates(): RunStateRow[]; - purgeByRepo(repoFullName: string): number; - close(): void; -}; -export declare const RUN_STATES: readonly RunState[]; -export declare function resolveRunStateDbPath(env?: Record): string; -/** - * Opens the 100% local/client-side miner run-state store. The database only lives on this machine; - * this module never uploads, syncs, or phones home with its contents. (#2289, #5563) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema migrations / purge still use the underlying DatabaseSync until those helpers are migrated. - * Public API stays synchronous so loop/CLI/MCP callers need no async cascade in this part-1 slice. - */ -export declare function initRunStateStore(dbPath?: string): RunStateStore; -export declare function getRunState(repoFullName: string, apiBaseUrl?: string): RunState | null; -export declare function setRunState(repoFullName: string, state: RunState, apiBaseUrl?: string): RunStateWrite; -export declare function listRunStates(): RunStateRow[]; -export declare function closeDefaultRunStateStore(): void; diff --git a/packages/loopover-miner/lib/run-state.js b/packages/loopover-miner/lib/run-state.js deleted file mode 100644 index b782a1412f..0000000000 --- a/packages/loopover-miner/lib/run-state.js +++ /dev/null @@ -1,171 +0,0 @@ -import { DEFAULT_FORGE_CONFIG } from "./forge-config.js"; -import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js"; -import { applySchemaMigrations } from "./schema-version.js"; -import { RUN_STATE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js"; -export const RUN_STATES = Object.freeze([ - "idle", - "discovering", - "planning", - "preparing", -]); -const runStateSet = new Set(RUN_STATES); -const defaultDbFileName = "run-state.sqlite3"; -let defaultRunStateStore = null; -function isRunState(value) { - return runStateSet.has(value); -} -export function resolveRunStateDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_RUN_STATE_DB", env); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveRunStateDbPath(), "invalid_run_state_db_path"); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const trimmed = repoFullName.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeRunState(state) { - if (runStateSet.has(state)) - return state; - throw new Error("invalid_run_state"); -} -/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563). - * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */ -function normalizeApiBaseUrl(apiBaseUrl) { - if (apiBaseUrl === undefined || apiBaseUrl === null) - return DEFAULT_FORGE_CONFIG.apiBaseUrl; - if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) - throw new Error("invalid_api_base_url"); - return apiBaseUrl.trim(); -} -// v1 -> v2 (#5563): rebuild the bare `repo_full_name` PRIMARY KEY into a (api_base_url, repo_full_name) composite -// -- two forge hosts serving a same-named owner/repo must not share one "current state" row. SQLite cannot ALTER -// a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy every existing row with the -// pre-#4784 implicit single-forge default backfilled, drop the old table, rename the new one in. -function addApiBaseUrlScope(db) { - db.exec(` - CREATE TABLE miner_run_state_v2 ( - api_base_url TEXT NOT NULL, - repo_full_name TEXT NOT NULL, - state TEXT NOT NULL CHECK (state IN ('idle', 'discovering', 'planning', 'preparing')), - updated_at TEXT NOT NULL, - PRIMARY KEY (api_base_url, repo_full_name) - ) - `); - // OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `state`, - // e.g. from a hand-edited or otherwise corrupted file -- getRunState/listRunStates fail closed on it too) - // would violate the CHECK constraint above and abort the whole migration. Skipping it here is consistent with - // that same fail-closed posture, rather than turning one bad row into a permanently unmigratable file. - db.prepare(`INSERT OR IGNORE INTO miner_run_state_v2 (api_base_url, repo_full_name, state, updated_at) - SELECT ?, repo_full_name, state, updated_at FROM miner_run_state`).run(DEFAULT_FORGE_CONFIG.apiBaseUrl); - db.exec("DROP TABLE miner_run_state"); - db.exec("ALTER TABLE miner_run_state_v2 RENAME TO miner_run_state"); -} -// v2 -> v3 (#4939): additive tenant-scoping column, a prerequisite for any hosted, multi-tenant use of this -// same store's logic. NULL for every row today -- self-host behavior is byte-identical, since nothing reads or -// writes it yet (no consumer exists until a future hosted deployment populates it). Same defensive -// column-presence guard as every other additive migration in this file's siblings (e.g. -// portfolio-queue.js's v3->v4 attempts_count addition). -function addTenantIdColumn(db) { - const hasTenantIdColumn = db - .prepare("PRAGMA table_info(miner_run_state)") - .all() - .some((column) => column.name === "tenant_id"); - if (!hasTenantIdColumn) - db.exec("ALTER TABLE miner_run_state ADD COLUMN tenant_id TEXT"); -} -/** - * Opens the 100% local/client-side miner run-state store. The database only lives on this machine; - * this module never uploads, syncs, or phones home with its contents. (#2289, #5563) - * - * Opened through the #7175 SqliteDriver seam (`openLocalStoreAdapter`): CRUD goes through `driver.query`, - * while schema migrations / purge still use the underlying DatabaseSync until those helpers are migrated. - * Public API stays synchronous so loop/CLI/MCP callers need no async cascade in this part-1 slice. - */ -export function initRunStateStore(dbPath = resolveRunStateDbPath()) { - const resolvedPath = normalizeDbPath(dbPath); - const { db, driver } = openLocalStoreAdapter(resolvedPath); - db.exec(` - CREATE TABLE IF NOT EXISTS miner_run_state ( - repo_full_name TEXT PRIMARY KEY, - state TEXT NOT NULL CHECK (state IN ('idle', 'discovering', 'planning', 'preparing')), - updated_at TEXT NOT NULL - ) - `); - // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations. - applySchemaMigrations(db, [addApiBaseUrlScope, addTenantIdColumn]); - const getSql = "SELECT state FROM miner_run_state WHERE api_base_url = ? AND repo_full_name = ?"; - const setSql = ` - INSERT INTO miner_run_state (api_base_url, repo_full_name, state, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(api_base_url, repo_full_name) DO UPDATE SET - state = excluded.state, - updated_at = excluded.updated_at - `; - const listSql = "SELECT api_base_url, repo_full_name, state, updated_at FROM miner_run_state ORDER BY repo_full_name"; - return { - dbPath: resolvedPath, - getRunState(repoFullName, apiBaseUrl) { - const { rows } = driver.query(getSql, [ - normalizeApiBaseUrl(apiBaseUrl), - normalizeRepoFullName(repoFullName), - ]); - const row = rows[0]; - const state = row?.state; - return isRunState(state) ? state : null; - }, - setRunState(repoFullName, state, apiBaseUrl) { - const normalizedForge = normalizeApiBaseUrl(apiBaseUrl); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const normalizedState = normalizeRunState(state); - const updatedAt = new Date().toISOString(); - driver.query(setSql, [normalizedForge, normalizedRepo, normalizedState, updatedAt]); - return { apiBaseUrl: normalizedForge, repoFullName: normalizedRepo, state: normalizedState, updatedAt }; - }, - /** Every repo with a recorded run state, across the whole store — the per-repo discover/plan/prepare - * signal a "run portfolio" view folds alongside managed PR rows (#4279). */ - listRunStates() { - const { rows } = driver.query(listSql, []); - return rows - .filter((row) => isRunState(row.state)) - .map((row) => ({ - apiBaseUrl: row.api_base_url, - repoFullName: row.repo_full_name, - state: row.state, - updatedAt: row.updated_at, - })); - }, - // Explicit, operator-invoked right-to-be-forgotten purge (#5564, #6599) — never runs automatically. - purgeByRepo(repoFullName) { - return purgeStoreByRepo(db, RUN_STATE_PURGE_SPEC, normalizeRepoFullName(repoFullName)); - }, - close() { - db.close(); - }, - }; -} -function getDefaultRunStateStore() { - defaultRunStateStore ??= initRunStateStore(); - return defaultRunStateStore; -} -export function getRunState(repoFullName, apiBaseUrl) { - return getDefaultRunStateStore().getRunState(repoFullName, apiBaseUrl); -} -export function setRunState(repoFullName, state, apiBaseUrl) { - return getDefaultRunStateStore().setRunState(repoFullName, state, apiBaseUrl); -} -export function listRunStates() { - return getDefaultRunStateStore().listRunStates(); -} -export function closeDefaultRunStateStore() { - if (!defaultRunStateStore) - return; - defaultRunStateStore.close(); - defaultRunStateStore = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVuLXN0YXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicnVuLXN0YXRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3pELE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxxQkFBcUIsRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzdHLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzVELE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBMkJoRixNQUFNLENBQUMsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN0QyxNQUFNO0lBQ04sYUFBYTtJQUNiLFVBQVU7SUFDVixXQUFXO0NBQ1osQ0FBd0IsQ0FBQztBQUUxQixNQUFNLFdBQVcsR0FBRyxJQUFJLEdBQUcsQ0FBUyxVQUFVLENBQUMsQ0FBQztBQUNoRCxNQUFNLGlCQUFpQixHQUFHLG1CQUFtQixDQUFDO0FBQzlDLElBQUksb0JBQW9CLEdBQXlCLElBQUksQ0FBQztBQUV0RCxTQUFTLFVBQVUsQ0FBQyxLQUFjO0lBQ2hDLE9BQU8sV0FBVyxDQUFDLEdBQUcsQ0FBQyxLQUFlLENBQUMsQ0FBQztBQUMxQyxDQUFDO0FBRUQsTUFBTSxVQUFVLHFCQUFxQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQ3pGLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsNkJBQTZCLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDeEYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUscUJBQXFCLEVBQUUsRUFBRSwyQkFBMkIsQ0FBQyxDQUFDO0FBQ2pHLENBQUM7QUFFRCxTQUFTLHFCQUFxQixDQUFDLFlBQW9CO0lBQ2pELElBQUksT0FBTyxZQUFZ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/schema-version.d.ts b/packages/loopover-miner/lib/schema-version.d.ts deleted file mode 100644 index 729e92da73..0000000000 --- a/packages/loopover-miner/lib/schema-version.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DatabaseSync } from "node:sqlite"; -/** A single post-baseline schema migration: mutate the store in place to advance it exactly one version. */ -export type SchemaMigration = (db: DatabaseSync) => void; -/** The bootstrap schema every store creates inline is, by convention, schema version 1. */ -export declare const BASELINE_SCHEMA_VERSION = 1; -/** Read a store's current `PRAGMA user_version`, coercing any absent/invalid value to 0 (pre-versioning). */ -export declare function readSchemaVersion(db: DatabaseSync): number; -/** - * Bring a store's on-disk schema up to date, then stamp its `user_version`. `migrations[i]` upgrades from - * version i+1 to i+2, so the target version is `BASELINE_SCHEMA_VERSION + migrations.length`. Every migration - * whose resulting version is above the file's current version runs, in order; a file already at (or past) the - * target runs none. Returns the resulting version. Never runs a migration twice (the stamped `user_version` - * gates re-runs on the next open) and never DOWNGRADES: a file written by newer code with more migrations is - * left at its higher version rather than stamped back down. Each migration and its version stamp are applied in - * one transaction, so a failure part-way through the sequence leaves the file at the last fully-applied version - * and re-opening resumes at the failed migration (a throwing migration rethrows after its changes roll back). - */ -export declare function applySchemaMigrations(db: DatabaseSync, migrations?: SchemaMigration[]): number; diff --git a/packages/loopover-miner/lib/schema-version.js b/packages/loopover-miner/lib/schema-version.js deleted file mode 100644 index 1ac409cf5d..0000000000 --- a/packages/loopover-miner/lib/schema-version.js +++ /dev/null @@ -1,54 +0,0 @@ -/** The bootstrap schema every store creates inline is, by convention, schema version 1. */ -export const BASELINE_SCHEMA_VERSION = 1; -/** Read a store's current `PRAGMA user_version`, coercing any absent/invalid value to 0 (pre-versioning). */ -export function readSchemaVersion(db) { - const row = db.prepare("PRAGMA user_version").get(); - const raw = row ? Number(row.user_version) : 0; - return Number.isInteger(raw) && raw >= 0 ? raw : 0; -} -/** - * Bring a store's on-disk schema up to date, then stamp its `user_version`. `migrations[i]` upgrades from - * version i+1 to i+2, so the target version is `BASELINE_SCHEMA_VERSION + migrations.length`. Every migration - * whose resulting version is above the file's current version runs, in order; a file already at (or past) the - * target runs none. Returns the resulting version. Never runs a migration twice (the stamped `user_version` - * gates re-runs on the next open) and never DOWNGRADES: a file written by newer code with more migrations is - * left at its higher version rather than stamped back down. Each migration and its version stamp are applied in - * one transaction, so a failure part-way through the sequence leaves the file at the last fully-applied version - * and re-opening resumes at the failed migration (a throwing migration rethrows after its changes roll back). - */ -export function applySchemaMigrations(db, migrations = []) { - const target = BASELINE_SCHEMA_VERSION + migrations.length; - const current = readSchemaVersion(db); - // A pre-versioning file (0) already holds the baseline schema, so advance from the baseline, not from 0. - const effective = current < BASELINE_SCHEMA_VERSION ? BASELINE_SCHEMA_VERSION : current; - // Stamp a pre-versioning file up to the baseline first, so a store with NO post-baseline migrations still - // records a version. Only ever stamp UPWARD: a file already at or past the baseline (including one written by - // newer code with more migrations) is never downgraded. `user_version` is an integer PRAGMA that cannot be - // parameterized; every stamped value here is a computed integer, never caller text, so interpolating is safe. - if (current < BASELINE_SCHEMA_VERSION) { - db.exec(`PRAGMA user_version = ${BASELINE_SCHEMA_VERSION}`); - } - for (let version = effective; version < target; version += 1) { - // Apply each migration AND stamp its resulting version in ONE transaction, so a failure part-way through the - // sequence leaves the file at the LAST fully-applied version: the next open resumes at the failed migration - // rather than re-running the ones that already succeeded (which, for a non-idempotent ALTER, would be a hard - // duplicate-column error). PRAGMA user_version is transactional in SQLite, so ROLLBACK undoes the migration's - // partial changes and its version stamp together. - db.exec("BEGIN"); - try { - const migration = migrations[version - BASELINE_SCHEMA_VERSION]; - // Index is in-range by construction (`version < target` and `target = BASELINE + migrations.length`). - migration(db); - db.exec(`PRAGMA user_version = ${version + 1}`); - db.exec("COMMIT"); - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - } - // The resulting on-disk version: `target` after an upgrade, or the file's own higher version when it was - // written by newer code (never downgraded). - return Math.max(current, target); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZW1hLXZlcnNpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzY2hlbWEtdmVyc2lvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFvQkEsMkZBQTJGO0FBQzNGLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsQ0FBQztBQUV6Qyw2R0FBNkc7QUFDN0csTUFBTSxVQUFVLGlCQUFpQixDQUFDLEVBQWdCO0lBQ2hELE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMscUJBQXFCLENBQUMsQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUNwRCxNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMvQyxPQUFPLE1BQU0sQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDckQsQ0FBQztBQUVEOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sVUFBVSxxQkFBcUIsQ0FBQyxFQUFnQixFQUFFLGFBQWdDLEVBQUU7SUFDeEYsTUFBTSxNQUFNLEdBQUcsdUJBQXVCLEdBQUcsVUFBVSxDQUFDLE1BQU0sQ0FBQztJQUMzRCxNQUFNLE9BQU8sR0FBRyxpQkFBaUIsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUN0Qyx5R0FBeUc7SUFDekcsTUFBTSxTQUFTLEdBQUcsT0FBTyxHQUFHLHVCQUF1QixDQUFDLENBQUMsQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDO0lBQ3hGLDBHQUEwRztJQUMxRyw4R0FBOEc7SUFDOUcsMkdBQTJHO0lBQzNHLDhHQUE4RztJQUM5RyxJQUFJLE9BQU8sR0FBRyx1QkFBdUIsRUFBRSxDQUFDO1FBQ3RDLEVBQUUsQ0FBQyxJQUFJLENBQUMseUJBQXlCLHVCQUF1QixFQUFFLENBQUMsQ0FBQztJQUM5RCxDQUFDO0lBQ0QsS0FBSyxJQUFJLE9BQU8sR0FBRyxTQUFTLEVBQUUsT0FBTyxHQUFHLE1BQU0sRUFBRSxPQUFPLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDN0QsNkdBQTZHO1FBQzdHLDRHQUE0RztRQUM1Ryw2R0FBNkc7UUFDN0csOEdBQThHO1FBQzlHLGtEQUFrRDtRQUNsRCxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2pCLElBQUksQ0FBQztZQUNILE1BQU0sU0FBUyxHQUFHLFVBQVUsQ0FBQyxPQUFPLEdBQUcsdUJBQXVCLENBQUMsQ0FBQztZQUNoRSxzR0FBc0c7WUFDdEcsU0FBVSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQ2YsRUFBRSxDQUFDLElBQUksQ0FBQyx5QkFBeUIsT0FBTyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDaEQsRUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUNwQixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUM7WUFDcEIsTUFBTSxLQUFLLENBQUM7UUFDZCxDQUFDO0lBQ0gsQ0FBQztJQUNELHlHQUF5RztJQUN6Ryw0Q0FBNEM7SUFDNUMsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQztBQUNuQyxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/self-review-context.d.ts b/packages/loopover-miner/lib/self-review-context.d.ts deleted file mode 100644 index 185db8a437..0000000000 --- a/packages/loopover-miner/lib/self-review-context.d.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { FocusManifest, SelfReviewContext } from "@loopover/engine"; -export type SelfReviewContextResult = Omit; -export type SelfReviewContextFetch = (url: string, init?: { - method?: string; - headers?: Record; - signal?: AbortSignal; -}) => Promise<{ - ok: boolean; - status: number; - json: () => Promise; - text: () => Promise; -}>; -export type LiveGateThresholdFields = { - confidence_floor: number | null; - scope_cap_files: number | null; - scope_cap_lines: number | null; -}; -export type LoopoverBackendSessionAuth = { - apiUrl?: string; - sessionToken: string; -}; -export type FetchSelfReviewContextOptions = { - githubToken?: string; - contributorLogin?: string; - linkedIssues?: number[]; - apiBaseUrl?: string; - rawContentBaseUrl?: string; - gittensorApiBase?: string; - fetchImpl?: SelfReviewContextFetch; - perPage?: number; - maxPages?: number; - requestTimeoutMs?: number; - /** Short ORB live-gate-thresholds probe budget (#6487). Default 400ms. */ - liveGateProbeTimeoutMs?: number; - /** Explicit session auth for the ORB probe; `null` forces standalone (skip probe). */ - loopoverAuth?: LoopoverBackendSessionAuth | null; - /** Env used to resolve loopover-mcp session when `loopoverAuth` is omitted. */ - env?: NodeJS.ProcessEnv; -}; -/** Validate the field-limited #6486/#6487 payload; null when nothing usable is present. */ -export declare function parseLiveGateThresholdFields(payload: unknown): LiveGateThresholdFields | null; -/** - * Overlay live ORB thresholds onto a statically-reconstructed FocusManifest (#6487). - * - confidence_floor → raise-only readinessMinScore (mirrors applySelfTuneOverrideToSettings). - * - scope_cap_files / scope_cap_lines → prefer live sizeMaxFiles / sizeMaxLines when present. - * Other gate fields are left untouched. - */ -export declare function applyLiveGateThresholdsToManifest(manifest: FocusManifest, fields: LiveGateThresholdFields | null): FocusManifest; -export declare function extractLinkedIssueNumbers(body: any, repoFullName: any): number[]; -/** - * Build a real SelfReviewContext from live GitHub data, at the same fidelity the live gate's own DB-backed - * construction produces. See this file's header for the one field (bounties) deliberately left undefined - * and why; issueQuality is populated from the live GitHub snapshot. Optionally overlays ORB live gate - * thresholds onto the static `.loopover.yml` reconstruction (#6487). - * - * @param {string} repoFullName - * @param {{ - * githubToken?: string, contributorLogin?: string, linkedIssues?: number[], - * apiBaseUrl?: string, rawContentBaseUrl?: string, gittensorApiBase?: string, - * fetchImpl?: typeof fetch, perPage?: number, maxPages?: number, requestTimeoutMs?: number, - * liveGateProbeTimeoutMs?: number, - * loopoverAuth?: { apiUrl?: string, sessionToken: string } | null, - * env?: NodeJS.ProcessEnv, - * }} [options] - * @returns {Promise} - */ -export declare function fetchSelfReviewContext(repoFullName: string, options?: FetchSelfReviewContextOptions): Promise; diff --git a/packages/loopover-miner/lib/self-review-context.js b/packages/loopover-miner/lib/self-review-context.js deleted file mode 100644 index a30dc34b06..0000000000 --- a/packages/loopover-miner/lib/self-review-context.js +++ /dev/null @@ -1,449 +0,0 @@ -import { buildCollisionReport, buildIssueQualityReport, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifestContent, } from "@loopover/engine"; -import { resolveLoopoverBackendSession } from "./github-token-resolution.js"; -// Real SelfReviewContext fetcher (#5145, Wave 3.5). Builds the context object the miner's self-review pass -// (packages/loopover-engine/src/miner/self-review-adapter.ts) needs, at the SAME fidelity the live gate's -// own DB-backed construction produces (src/db/repositories.ts's toRepositoryRecord/toIssueRecord/ -// toPullRequestRecord) -- just built fresh from live GitHub data instead of a DB round-trip, since the miner -// has no database. One of SelfReviewContext's eight fields is DELIBERATELY left undefined, not stubbed: -// -// - `bounties`: bounty data is not GitHub-native in this codebase -- it comes from an external "Gitt" -// system that PUSHES data into the live gate's own internal ingest route (src/api/routes.ts). There is -// no public endpoint the miner could legitimately pull from instead. -// -// `issueQuality` is populated via buildIssueQualityReport (exported from @loopover/engine as a package-local -// twin of the host engine helper — see #6057). Bounty rows and recent-merged PR history are passed as empty -// arrays because this fetcher does not yet pull either source. `bounties` remains omitted for the reason above. -// -// #6487: after the static `.loopover.yml` reconstruction, optionally probe ORB's live-gate-thresholds endpoint -// (same loopover-mcp session posture as resolveGitHubToken). On success, overlay confidence_floor / -// scope_cap_files / scope_cap_lines onto the parsed manifest gate; on 403/timeout/404/no-session, keep the -// static reconstruction unchanged. Fully-standalone (ORB-absent) paths stay byte-identical. -const GITHUB_API_VERSION = "2022-11-28"; -const DEFAULT_API_BASE_URL = "https://api.github.com"; -const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com"; -const DEFAULT_GITTENSOR_API_BASE = "https://api.gittensor.io"; -const DEFAULT_PER_PAGE = 100; -const DEFAULT_MAX_PAGES = 10; -const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; -/** Short ORB probe budget (#6487) — must never make discover/gate-prediction meaningfully slower when ORB is absent. */ -const DEFAULT_LIVE_GATE_PROBE_TIMEOUT_MS = 400; -// Mirrors src/signals/focus-manifest-loader.ts's MANIFEST_FILE_CANDIDATES exactly -- first candidate that -// resolves wins, same as the live gate's own lookup order. -const MANIFEST_FILE_CANDIDATES = [".loopover.yml", ".github/loopover.yml", ".loopover.json", ".github/loopover.json"]; -function parseRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - return null; - const [owner, repo, extra] = repoFullName.split("/"); - if (!owner || !repo || extra !== undefined) - return null; - return { owner, repo }; -} -function githubHeaders(githubToken) { - const headers = { - accept: "application/vnd.github+json", - "user-agent": "loopover-miner", - "x-github-api-version": GITHUB_API_VERSION, - }; - const token = typeof githubToken === "string" ? githubToken.trim() : ""; - if (token) - headers.authorization = `Bearer ${token}`; - return headers; -} -function normalizeOptions(options = {}) { - const env = options.env ?? process.env; - // Explicit null skips the probe (tests / forced-standalone). Undefined ⇒ resolve from loopover-mcp session. - const loopoverAuth = options.loopoverAuth === null - ? null - : options.loopoverAuth && typeof options.loopoverAuth.sessionToken === "string" && options.loopoverAuth.sessionToken - ? { - apiUrl: typeof options.loopoverAuth.apiUrl === "string" && options.loopoverAuth.apiUrl.trim() - ? options.loopoverAuth.apiUrl.replace(/\/+$/, "") - : (resolveLoopoverBackendSession(env)?.apiUrl ?? "https://api.loopover.ai"), - sessionToken: options.loopoverAuth.sessionToken, - } - : resolveLoopoverBackendSession(env); - return { - githubToken: options.githubToken ?? env.GITHUB_TOKEN ?? "", - apiBaseUrl: typeof options.apiBaseUrl === "string" && options.apiBaseUrl.trim() ? options.apiBaseUrl.trim() : DEFAULT_API_BASE_URL, - rawContentBaseUrl: typeof options.rawContentBaseUrl === "string" && options.rawContentBaseUrl.trim() ? options.rawContentBaseUrl.trim() : DEFAULT_RAW_CONTENT_BASE_URL, - gittensorApiBase: typeof options.gittensorApiBase === "string" && options.gittensorApiBase.trim() ? options.gittensorApiBase.trim() : DEFAULT_GITTENSOR_API_BASE, - fetchImpl: (options.fetchImpl ?? fetch), - perPage: Number.isInteger(options.perPage) && options.perPage > 0 ? options.perPage : DEFAULT_PER_PAGE, - maxPages: Number.isInteger(options.maxPages) && options.maxPages > 0 ? options.maxPages : DEFAULT_MAX_PAGES, - contributorLogin: typeof options.contributorLogin === "string" ? options.contributorLogin.trim() : "", - linkedIssues: Array.isArray(options.linkedIssues) ? options.linkedIssues.filter((n) => Number.isInteger(n)) : [], - requestTimeoutMs: Number.isInteger(options.requestTimeoutMs) && options.requestTimeoutMs > 0 ? options.requestTimeoutMs : DEFAULT_REQUEST_TIMEOUT_MS, - liveGateProbeTimeoutMs: Number.isInteger(options.liveGateProbeTimeoutMs) && options.liveGateProbeTimeoutMs > 0 - ? options.liveGateProbeTimeoutMs - : DEFAULT_LIVE_GATE_PROBE_TIMEOUT_MS, - loopoverAuth, - }; -} -/** Validate the field-limited #6486/#6487 payload; null when nothing usable is present. */ -export function parseLiveGateThresholdFields(payload) { - if (!payload || typeof payload !== "object" || Array.isArray(payload)) - return null; - const record = payload; - const confidence_floor = typeof record.confidence_floor === "number" && record.confidence_floor >= 0 && record.confidence_floor <= 1 - ? record.confidence_floor - : null; - const scope_cap_files = typeof record.scope_cap_files === "number" && record.scope_cap_files > 0 ? record.scope_cap_files : null; - const scope_cap_lines = typeof record.scope_cap_lines === "number" && record.scope_cap_lines > 0 ? record.scope_cap_lines : null; - if (confidence_floor === null && scope_cap_files === null && scope_cap_lines === null) - return null; - return { confidence_floor, scope_cap_files, scope_cap_lines }; -} -/** - * Overlay live ORB thresholds onto a statically-reconstructed FocusManifest (#6487). - * - confidence_floor → raise-only readinessMinScore (mirrors applySelfTuneOverrideToSettings). - * - scope_cap_files / scope_cap_lines → prefer live sizeMaxFiles / sizeMaxLines when present. - * Other gate fields are left untouched. - */ -export function applyLiveGateThresholdsToManifest(manifest, fields) { - if (!manifest || !fields) - return manifest; - const gate = { ...manifest.gate }; - if (typeof fields.confidence_floor === "number") { - const floorScore = Math.max(0, Math.min(100, Math.round(fields.confidence_floor * 100))); - if (typeof gate.readinessMinScore === "number" && floorScore > gate.readinessMinScore) { - gate.readinessMinScore = floorScore; - } - } - if (typeof fields.scope_cap_files === "number" && fields.scope_cap_files > 0) { - gate.sizeMaxFiles = fields.scope_cap_files; - } - if (typeof fields.scope_cap_lines === "number" && fields.scope_cap_lines > 0) { - gate.sizeMaxLines = fields.scope_cap_lines; - } - return { ...manifest, gate }; -} -async function probeLiveGateThresholds(target, resolved) { - const auth = resolved.loopoverAuth; - if (!auth?.sessionToken) - return null; - const url = `${auth.apiUrl}/v1/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/live-gate-thresholds`; - try { - const response = await fetchWithTimeout(resolved.fetchImpl, url, { - method: "GET", - headers: { - authorization: `Bearer ${auth.sessionToken}`, - accept: "application/json", - "user-agent": "loopover-miner", - }, - }, resolved.liveGateProbeTimeoutMs); - if (!response.ok) - return null; - const payload = await response.json().catch(() => null); - return parseLiveGateThresholdFields(payload); - } - catch { - return null; - } -} -// A fresh AbortSignal.timeout() per call, so a stalled connection can't hang context construction forever -// (#miner-github-read-timeouts) -- shared by this file's three independent fetch call sites (GitHub REST, raw -// manifest content, the Gittensor contributor lookup). -async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) { - return fetchImpl(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }); -} -async function githubGetJson(url, resolved) { - const response = await fetchWithTimeout(resolved.fetchImpl, url, { method: "GET", headers: githubHeaders(resolved.githubToken) }, resolved.requestTimeoutMs); - const payload = await response.json().catch(() => null); - return { response, payload }; -} -async function fetchPaginated(path, query, resolved) { - const results = []; - for (let page = 1; page <= resolved.maxPages; page += 1) { - const params = new URLSearchParams({ ...query, per_page: String(resolved.perPage), page: String(page) }); - const url = `${resolved.apiBaseUrl}${path}?${params}`; - const { response, payload } = await githubGetJson(url, resolved); - if (!response.ok || !Array.isArray(payload)) - break; - results.push(...payload); - if (payload.length < resolved.perPage) - break; - } - return results; -} -// Mirrors src/db/repositories.ts's toRepositoryRecord + upsertRepositoryFromGitHub's field mapping. The -// miner has no App installation/DB, so installationId/isInstalled/isRegistered/registryConfig are honest -// "unregistered" defaults, not values pulled from GitHub -- GitHub's own repo payload carries none of them. -async function fetchRepositoryRecord(target, resolved) { - const url = `${resolved.apiBaseUrl}/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}`; - const { response, payload } = await githubGetJson(url, resolved); - if (!response.ok || !payload || typeof payload !== "object") - return null; - return { - fullName: `${target.owner}/${target.repo}`, - owner: payload.owner?.login ?? target.owner, - name: payload.name ?? target.repo, - installationId: undefined, - isInstalled: false, - isRegistered: false, - isPrivate: payload.private ?? false, - htmlUrl: payload.html_url ?? null, - defaultBranch: payload.default_branch ?? null, - registryConfig: null, - }; -} -// Mirrors src/db/repositories.ts's extractLinkedPrNumbers: a real link needs a CLOSING KEYWORD, not a bare -// mention (#6769). Without the keyword prefix, an incidental "similar to what we saw in PR #501" in an issue -// body counted as a linked PR, so the issue-quality report read the issue as "already references a PR" and the -// miner skipped an available issue (the host's own #issue-body-pr-mention-pollution fix, never ported here). -const LINKED_PR_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:PR|pull request)\s+#(\d+)\b/gi; -function extractLinkedPrNumbers(body) { - const numbers = []; - for (const match of body.matchAll(LINKED_PR_PATTERN)) { - const number = Number(match[1]); - if (Number.isInteger(number) && number > 0) - numbers.push(number); - } - return numbers; -} -// Mirrors src/db/repositories.ts's extractLinkedIssueNumbersWithOverflow (#7527): GitHub's own closing-keyword -// vocabulary, counting a fully-qualified `owner/repo#N` OR a full-`https://github.com/owner/repo/issues/N` -// reference only when it targets the SAME repo being fetched. Like the host, code spans are excluded by BYTE -// RANGE, not by string-replacing them: replacing a span with whitespace would let the text on either side of -// the removed span combine into a fake closing reference (e.g. "Fixes `x` #45" -> "Fixes #45", which the two -// surrounding spaces then make match). This lighter port intentionally omits the host's 50-item overflow cap + -// dedup set (the miner scans only its own small PR body), but is otherwise in sync on the matching semantics. -const LINKED_ISSUE_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https?:\/\/(?:www\.)?github\.com\/(?[\w.-]+\/[\w.-]+)\/issues\/(?\d+)|(?[\w.-]+\/[\w.-]+)#(?\d+)|#(?\d+))\b/gi; -export function extractLinkedIssueNumbers(body, repoFullName) { - // Byte ranges of every inline code span; a keyword match whose range overlaps one is a quoted example, not a - // real closing directive (see the comment above for why this is a range check, not a string strip). - const inlineCodeSpanRanges = [...body.matchAll(/`[^`\n]*`/g)].map((match) => ({ - start: match.index, - end: match.index + match[0].length, - })); - const numbers = []; - const normalizedRepo = repoFullName.toLowerCase(); - for (const match of body.matchAll(LINKED_ISSUE_PATTERN)) { - const matchStart = match.index; - const matchEnd = matchStart + match[0].length; - if (inlineCodeSpanRanges.some((range) => matchStart < range.end && matchEnd > range.start)) - continue; - const groups = match.groups; - // Both the qualified and URL forms carry an owner/repo that must match THIS repo; the bare `#N` form has no - // owner and always counts. A reference to a different repo closes an issue there, not here. - const owner = groups.urlOwner ?? groups.qualOwner; - if (owner && owner.toLowerCase() !== normalizedRepo) - continue; - const number = Number(groups.urlNum ?? groups.qualNum ?? groups.bareNum); - if (Number.isInteger(number) && number > 0) - numbers.push(number); - } - return numbers; -} -function labelNames(labels) { - if (!Array.isArray(labels)) - return []; - return labels.flatMap((label) => (label && typeof label === "object" && typeof label.name === "string" ? [label.name] : [])); -} -// Mirrors src/db/repositories.ts's toIssueRecord, populated straight from the live payload (createdAt/ -// updatedAt/closedAt come from the DB-row read path there only as a caching artifact, not a semantic -// transform -- the live REST fields are the real source). -function toIssueRecord(repoFullName, issue) { - const body = issue.body ?? ""; - return { - repoFullName, - number: issue.number, - title: issue.title, - state: issue.state, - authorLogin: issue.user?.login ?? null, - authorAssociation: issue.author_association ?? null, - htmlUrl: issue.html_url ?? null, - body, - createdAt: issue.created_at ?? null, - updatedAt: issue.updated_at ?? null, - closedAt: issue.closed_at ?? null, - labels: labelNames(issue.labels), - linkedPrs: extractLinkedPrNumbers(body), - }; -} -async function fetchOpenIssueRecords(target, resolved) { - const payloads = await fetchPaginated(`/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/issues`, { state: "open", sort: "created", direction: "asc" }, resolved); - // GitHub's Issues endpoint also returns pull requests -- filter them out, same as the live gate's own fetch. - return payloads.filter((issue) => issue && typeof issue === "object" && !issue.pull_request).map((issue) => toIssueRecord(`${target.owner}/${target.repo}`, issue)); -} -function mergeableBooleanState(mergeable) { - if (mergeable === true) - return "clean"; - if (mergeable === false) - return "dirty"; - return null; -} -// Mirrors src/db/repositories.ts's toPullRequestRecord. Only the fields SelfReviewContext/buildCollisionReport -// actually consume are populated with real precision; merge/RC3 gate-plumbing fields the live gate's fuller -// PullRequestRecord carries (mergeAttemptCount, approvedHeadSha, ...) don't exist on the engine package's -// leaner mirror type and aren't meaningful for a miner attempt anyway. -function toPullRequestRecord(repoFullName, pr) { - const body = pr.body ?? ""; - return { - repoFullName, - number: pr.number, - title: pr.title, - state: pr.state, - authorLogin: pr.user?.login ?? null, - authorAssociation: pr.author_association ?? null, - headSha: pr.head?.sha ?? null, - headRef: pr.head?.ref ?? null, - baseRef: pr.base?.ref ?? null, - htmlUrl: pr.html_url ?? null, - mergedAt: pr.merged_at ?? null, - isDraft: pr.draft ?? null, - mergeableState: pr.mergeable_state ?? mergeableBooleanState(pr.mergeable), - reviewDecision: null, - body, - createdAt: pr.created_at ?? null, - updatedAt: pr.updated_at ?? null, - closedAt: pr.closed_at ?? null, - labels: labelNames(pr.labels), - linkedIssues: extractLinkedIssueNumbers(body, repoFullName), - }; -} -async function fetchOpenPullRequestRecords(target, resolved) { - const payloads = await fetchPaginated(`/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/pulls`, { state: "open", sort: "created", direction: "asc" }, resolved); - return payloads.map((pr) => toPullRequestRecord(`${target.owner}/${target.repo}`, pr)); -} -// Mirrors src/signals/focus-manifest-loader.ts's raw-content lookup order and bounded body read: -// first candidate path that resolves wins, but hostile manifests never exceed the parser byte cap in memory. -async function readBoundedManifestResponseText(response) { - const contentLength = response.headers?.get?.("content-length") ?? null; - if (contentLength !== null) { - const parsedLength = Number.parseInt(contentLength, 10); - if (Number.isFinite(parsedLength) && parsedLength > MAX_FOCUS_MANIFEST_BYTES) - return null; - } - if (!response.body?.getReader) { - const text = await response.text(); - if (typeof text !== "string") - return null; - return new TextEncoder().encode(text).byteLength > MAX_FOCUS_MANIFEST_BYTES ? null : text; - } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let totalBytes = 0; - let text = ""; - try { - while (true) { - const { done, value } = await reader.read(); - if (done) - break; - totalBytes += value.byteLength; - if (totalBytes > MAX_FOCUS_MANIFEST_BYTES) { - await reader.cancel(); - return null; - } - text += decoder.decode(value, { stream: true }); - } - text += decoder.decode(); - return text; - } - finally { - reader.releaseLock(); - } -} -async function fetchManifestContent(target, resolved) { - for (const path of MANIFEST_FILE_CANDIDATES) { - const url = `${resolved.rawContentBaseUrl}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/HEAD/${path}`; - try { - const response = await fetchWithTimeout(resolved.fetchImpl, url, { method: "GET", headers: { accept: "application/json", "user-agent": "loopover-miner" } }, resolved.requestTimeoutMs); - if (response.ok) { - const text = await readBoundedManifestResponseText(response); - if (typeof text === "string") - return text; - } - } - catch { - // Try the next candidate path. - } - } - return null; -} -// Mirrors src/gittensor/api.ts's fetchGittensorContributorSnapshot/fetchOfficialGittensorMiner: a public, -// unauthenticated GET against the Gittensor API (not GitHub) -- confirmed only when a real entry with a -// matching GitHub login is found; any transport/parse failure fails closed to "not confirmed", never throws. -async function fetchConfirmedContributor(login, resolved) { - if (!login) - return false; - try { - const response = await fetchWithTimeout(resolved.fetchImpl, `${resolved.gittensorApiBase}/miners`, { method: "GET", headers: { accept: "application/json" } }, resolved.requestTimeoutMs); - if (!response.ok) - return false; - const payload = await response.json().catch(() => null); - if (!Array.isArray(payload)) - return false; - const normalizedLogin = login.toLowerCase(); - return payload.some((miner) => typeof miner?.githubUsername === "string" && miner.githubUsername.toLowerCase() === normalizedLogin); - } - catch { - return false; - } -} -// Per self-review-adapter.ts's own doc comment: the caller computes inDuplicateCluster "the same way the -// live gate's collision report would" -- adapted from src/signals/engine.ts's real -// isPullRequestInDuplicateCluster (root src/, not extracted to the engine package), which requires >= 2 -// PULL REQUEST items in a high-risk cluster, not just any high-risk cluster containing the target. That -// threshold matters: buildCollisionReport's own pairwise "shared linked issue" rule already marks an -// issue+its-one-legitimately-closing-PR pair as a HIGH-risk cluster (confirmed empirically) -- without the -// >= 2 threshold, inDuplicateCluster would fire on the completely normal case of "one PR already closes -// this issue," not genuine overlapping/duplicate work. Checks the target ISSUE's presence instead of a -// not-yet-existing PR number, since the miner's own submission doesn't exist as a real PullRequestRecord yet. -// Takes a prebuilt CollisionReport so issueQuality and inDuplicateCluster share one collision pass. -function computeInDuplicateCluster(collisionReport, targetIssueNumbers) { - if (targetIssueNumbers.length === 0) - return false; - return collisionReport.clusters.some((cluster) => cluster.risk === "high" && - cluster.items.filter((item) => item.type === "pull_request").length >= 2 && - cluster.items.some((item) => item.type === "issue" && targetIssueNumbers.includes(item.number))); -} -/** - * Build a real SelfReviewContext from live GitHub data, at the same fidelity the live gate's own DB-backed - * construction produces. See this file's header for the one field (bounties) deliberately left undefined - * and why; issueQuality is populated from the live GitHub snapshot. Optionally overlays ORB live gate - * thresholds onto the static `.loopover.yml` reconstruction (#6487). - * - * @param {string} repoFullName - * @param {{ - * githubToken?: string, contributorLogin?: string, linkedIssues?: number[], - * apiBaseUrl?: string, rawContentBaseUrl?: string, gittensorApiBase?: string, - * fetchImpl?: typeof fetch, perPage?: number, maxPages?: number, requestTimeoutMs?: number, - * liveGateProbeTimeoutMs?: number, - * loopoverAuth?: { apiUrl?: string, sessionToken: string } | null, - * env?: NodeJS.ProcessEnv, - * }} [options] - * @returns {Promise} - */ -export async function fetchSelfReviewContext(repoFullName, options = {}) { - const target = parseRepoFullName(repoFullName); - if (!target) - throw new Error("invalid_repo_full_name"); - const resolved = normalizeOptions(options); - const [repo, issues, pullRequests, manifestContent, confirmedContributor, liveGateThresholds] = await Promise.all([ - fetchRepositoryRecord(target, resolved), - fetchOpenIssueRecords(target, resolved), - fetchOpenPullRequestRecords(target, resolved), - fetchManifestContent(target, resolved), - fetchConfirmedContributor(resolved.contributorLogin, resolved), - probeLiveGateThresholds(target, resolved), - ]); - const staticManifest = parseFocusManifestContent(manifestContent, "repo_file"); - const manifest = applyLiveGateThresholdsToManifest(staticManifest, liveGateThresholds); - // Positional args match buildIssueQualityReport(repo, issues, pullRequests, fullName, bounties, collisions, recentMerged): - // repo is the full RepositoryRecord from fetchRepositoryRecord (not a string); empty bounties/recentMerged - // because this fetcher has no external bounty source and does not yet pull merge history. - const fullName = `${target.owner}/${target.repo}`; - const collisions = buildCollisionReport(fullName, issues, pullRequests); - const inDuplicateCluster = computeInDuplicateCluster(collisions, resolved.linkedIssues); - const issueQuality = buildIssueQualityReport(repo, issues, pullRequests, fullName, [], collisions, []); - return { - manifest, - repo, - issues, - pullRequests, - confirmedContributor, - inDuplicateCluster, - issueQuality, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VsZi1yZXZpZXctY29udGV4dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNlbGYtcmV2aWV3LWNvbnRleHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUNMLG9CQUFvQixFQUNwQix1QkFBdUIsRUFDdkIsd0JBQXdCLEVBQ3hCLHlCQUF5QixHQUMxQixNQUFNLGtCQUFrQixDQUFDO0FBRTFCLE9BQU8sRUFBRSw2QkFBNkIsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBa0Q3RSwyR0FBMkc7QUFDM0csMEdBQTBHO0FBQzFHLGtHQUFrRztBQUNsRyw2R0FBNkc7QUFDN0csd0dBQXdHO0FBQ3hHLEVBQUU7QUFDRix3R0FBd0c7QUFDeEcsMkdBQTJHO0FBQzNHLHlFQUF5RTtBQUN6RSxFQUFFO0FBQ0YsNkdBQTZHO0FBQzdHLDRHQUE0RztBQUM1RyxnSEFBZ0g7QUFDaEgsRUFBRTtBQUNGLCtHQUErRztBQUMvRyxvR0FBb0c7QUFDcEcsMkdBQTJHO0FBQzNHLDRGQUE0RjtBQUU1RixNQUFNLGtCQUFrQixHQUFHLFlBQVksQ0FBQztBQUN4QyxNQUFNLG9CQUFvQixHQUFHLHdCQUF3QixDQUFDO0FBQ3RELE1BQU0sNEJBQTRCLEdBQUcsbUNBQW1DLENBQUM7QUFDekUsTUFBTSwwQkFBMEIsR0FBRywwQkFBMEIsQ0FBQztBQUM5RCxNQUFNLGdCQUFnQixHQUFHLEdBQUcsQ0FBQztBQUM3QixNQUFNLGlCQUFpQixHQUFHLEVBQUUsQ0FBQztBQUM3QixNQUFNLDBCQUEwQixHQUFHLE1BQU0sQ0FBQztBQUMxQyx3SEFBd0g7QUFDeEgsTUFBTSxrQ0FBa0MsR0FBRyxHQUFHLENBQUM7QUFFL0MsMEdBQTBHO0FBQzFHLDJEQUEyRDtBQUMzRCxNQUFNLHdCQUF3QixHQUFHLENBQUMsZUFBZSxFQUFFLHNCQUFzQixFQUFFLGdCQUFnQixFQUFFLHVCQUF1QixDQUFDLENBQUM7QUFFdEgsU0FBUyxpQkFBaUIsQ0FBQyxZQUFpQjtJQUMxQyxJ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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/sentry.d.ts b/packages/loopover-miner/lib/sentry.d.ts deleted file mode 100644 index ece789250c..0000000000 --- a/packages/loopover-miner/lib/sentry.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** Opt-in Sentry error tracking for the miner CLI (#6011). Complete no-op unless LOOPOVER_MINER_SENTRY_DSN is - * set -- an operator points this at their OWN Sentry project; this is a published, independently-installed CLI - * (@loopover/miner), so nothing here is ever auto-enabled or phones home by default, mirroring the main repo's - * self-host Sentry integration (src/selfhost/sentry.ts). `@sentry/node` is lazy-imported only inside - * `initMinerSentry()` so a miner invocation that never opts in pays zero module-load cost -- this CLI runs very - * frequently under an unattended loop (lib/loop-cli.js). Unlike the main repo, there is no structured JSON-log - * forwarding here: this package's own logger (lib/logger.js) writes plain `key=value` lines, not JSON, so - * capture is explicit (`captureMinerError`) at each call site rather than a console-override. */ -/** Initialize Sentry from `env` (default `process.env`). Returns whether it activated. Call once, as early as - * possible in a bin's startup -- after `loadMinerFileSecrets()` (so a `_FILE`-mounted DSN resolves first) and - * before `installCliSignalHandlers()` (so a startup crash is still captured). */ -export declare function initMinerSentry(env?: Record): Promise; -/** Capture an error with optional structured context. No-op when Sentry is off. Never throws. */ -export declare function captureMinerError(error: unknown, context?: Record): void; -/** Flush buffered events before the process exits. No-op when off. Never throws or hangs past `timeoutMs`. */ -export declare function flushMinerSentry(timeoutMs?: number): Promise; -/** Capture AND flush before returning -- the crash-path convenience wrapper for - * installCliSignalHandlers' `captureError` hook (process-lifecycle.js). A bare `captureMinerError()` only - * QUEUES the event in Sentry's transport; `process.exit()` tears the process down immediately afterward - * without waiting for any pending HTTP delivery, so the crash-capture path needs this awaited flush or it is - * very likely a near-total no-op in practice. */ -export declare function captureMinerErrorAndFlush(error: unknown, context?: Record): Promise; -/** Test-only: reset module state so one test's activation can't leak into the next. */ -export declare function resetMinerSentryForTesting(): void; diff --git a/packages/loopover-miner/lib/sentry.js b/packages/loopover-miner/lib/sentry.js deleted file mode 100644 index ef6f6b5349..0000000000 --- a/packages/loopover-miner/lib/sentry.js +++ /dev/null @@ -1,66 +0,0 @@ -/** Opt-in Sentry error tracking for the miner CLI (#6011). Complete no-op unless LOOPOVER_MINER_SENTRY_DSN is - * set -- an operator points this at their OWN Sentry project; this is a published, independently-installed CLI - * (@loopover/miner), so nothing here is ever auto-enabled or phones home by default, mirroring the main repo's - * self-host Sentry integration (src/selfhost/sentry.ts). `@sentry/node` is lazy-imported only inside - * `initMinerSentry()` so a miner invocation that never opts in pays zero module-load cost -- this CLI runs very - * frequently under an unattended loop (lib/loop-cli.js). Unlike the main repo, there is no structured JSON-log - * forwarding here: this package's own logger (lib/logger.js) writes plain `key=value` lines, not JSON, so - * capture is explicit (`captureMinerError`) at each call site rather than a console-override. */ -let Sentry; -let active = false; -/** Initialize Sentry from `env` (default `process.env`). Returns whether it activated. Call once, as early as - * possible in a bin's startup -- after `loadMinerFileSecrets()` (so a `_FILE`-mounted DSN resolves first) and - * before `installCliSignalHandlers()` (so a startup crash is still captured). */ -export async function initMinerSentry(env = process.env) { - if (!env.LOOPOVER_MINER_SENTRY_DSN) - return false; - const mod = await import("@sentry/node"); - Sentry = mod; - Sentry.init({ - dsn: env.LOOPOVER_MINER_SENTRY_DSN, - environment: env.LOOPOVER_MINER_SENTRY_ENVIRONMENT ?? "production", - }); - active = true; - return true; -} -/** Capture an error with optional structured context. No-op when Sentry is off. Never throws. */ -export function captureMinerError(error, context) { - if (!active || !Sentry) - return; - try { - Sentry.withScope((scope) => { - if (context) - scope.setContext("miner", context); - Sentry.captureException(error instanceof Error ? error : new Error(String(error))); - }); - } - catch { - /* Sentry capture must never crash the caller it's instrumenting. */ - } -} -/** Flush buffered events before the process exits. No-op when off. Never throws or hangs past `timeoutMs`. */ -export async function flushMinerSentry(timeoutMs = 2000) { - if (!active || !Sentry) - return; - try { - await Sentry.flush(timeoutMs); - } - catch { - /* Best-effort -- a flush failure must never block process exit. */ - } -} -/** Capture AND flush before returning -- the crash-path convenience wrapper for - * installCliSignalHandlers' `captureError` hook (process-lifecycle.js). A bare `captureMinerError()` only - * QUEUES the event in Sentry's transport; `process.exit()` tears the process down immediately afterward - * without waiting for any pending HTTP delivery, so the crash-capture path needs this awaited flush or it is - * very likely a near-total no-op in practice. */ -export async function captureMinerErrorAndFlush(error, context) { - captureMinerError(error, context); - await flushMinerSentry(); -} -/** Test-only: reset module state so one test's activation can't leak into the next. */ -export function resetMinerSentryForTesting() { - Sentry = undefined; - active = false; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VudHJ5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic2VudHJ5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7O2lHQU9pRztBQUlqRyxJQUFJLE1BQTRCLENBQUM7QUFDakMsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDO0FBRW5COztpRkFFaUY7QUFDakYsTUFBTSxDQUFDLEtBQUssVUFBVSxlQUFlLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7SUFDekYsSUFBSSxDQUFDLEdBQUcsQ0FBQyx5QkFBeUI7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUNqRCxNQUFNLEdBQUcsR0FBRyxNQUFNLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQztJQUN6QyxNQUFNLEdBQUcsR0FBRyxDQUFDO0lBQ2IsTUFBTSxDQUFDLElBQUksQ0FBQztRQUNWLEdBQUcsRUFBRSxHQUFHLENBQUMseUJBQXlCO1FBQ2xDLFdBQVcsRUFBRSxHQUFHLENBQUMsaUNBQWlDLElBQUksWUFBWTtLQUNuRSxDQUFDLENBQUM7SUFDSCxNQUFNLEdBQUcsSUFBSSxDQUFDO0lBQ2QsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBRUQsaUdBQWlHO0FBQ2pHLE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxLQUFjLEVBQUUsT0FBaUM7SUFDakYsSUFBSSxDQUFDLE1BQU0sSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPO0lBQy9CLElBQUksQ0FBQztRQUNILE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRTtZQUN6QixJQUFJLE9BQU87Z0JBQUUsS0FBSyxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDaEQsTUFBTyxDQUFDLGdCQUFnQixDQUFDLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN0RixDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxvRUFBb0U7SUFDdEUsQ0FBQztBQUNILENBQUM7QUFFRCw4R0FBOEc7QUFDOUcsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxTQUFTLEdBQUcsSUFBSTtJQUNyRCxJQUFJLENBQUMsTUFBTSxJQUFJLENBQUMsTUFBTTtRQUFFLE9BQU87SUFDL0IsSUFBSSxDQUFDO1FBQ0gsTUFBTSxNQUFNLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ2hDLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxtRUFBbUU7SUFDckUsQ0FBQztBQUNILENBQUM7QUFFRDs7OztpREFJaUQ7QUFDakQsTUFBTSxDQUFDLEtBQUssVUFBVSx5QkFBeUIsQ0FBQyxLQUFjLEVBQUUsT0FBaUM7SUFDL0YsaUJBQWlCLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ2xDLE1BQU0sZ0JBQWdCLEVBQUUsQ0FBQztBQUMzQixDQUFDO0FBRUQsdUZBQXVGO0FBQ3ZGLE1BQU0sVUFBVSwwQkFBMEI7SUFDeEMsTUFBTSxHQUFHLFNBQVMsQ0FBQztJQUNuQixNQUFNLEdBQUcsS0FBSyxDQUFDO0FBQ2pCLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/slop-assessment.d.ts b/packages/loopover-miner/lib/slop-assessment.d.ts deleted file mode 100644 index fd4f17bdf3..0000000000 --- a/packages/loopover-miner/lib/slop-assessment.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { SlopAssessment, SlopAssessmentInput } from "@loopover/engine"; -export declare function runSlopAssessment(input: SlopAssessmentInput): SlopAssessment; diff --git a/packages/loopover-miner/lib/slop-assessment.js b/packages/loopover-miner/lib/slop-assessment.js deleted file mode 100644 index fa18443839..0000000000 --- a/packages/loopover-miner/lib/slop-assessment.js +++ /dev/null @@ -1,15 +0,0 @@ -import { buildSlopAssessment } from "@loopover/engine"; -// Production runSlopAssessment binding (#5133, Wave 3.5 follow-up to #2334). `attempt-runner.js`'s -// `deps.runSlopAssessment` (via #2333's iterate-loop -> self-review-adapter's `SelfReviewAdapterDeps`) had -// no production implementation anywhere in this package -- only the test double in -// `test/unit/miner-attempt-runner.test.ts` existed. `packages/loopover-engine/src/miner/self-review-adapter.ts`'s -// own header comment already anticipated this exact binding: `SelfReviewSlopInput`/`SelfReviewSlopAssessment` -// are a deliberate, hand-kept STRUCTURAL MIRROR of `buildSlopAssessment`'s own `SlopAssessmentInput`/ -// `SlopAssessment` (down to reusing the SAME canonical `AdvisoryFinding` type for `findings`), specifically so -// a real binding could be a direct pass-through with no mapping logic once the deterministic scorer itself -// became portable -- which #5133 did (`src/signals/slop.ts`'s PR-side scorer is now extracted to -// `packages/loopover-engine/src/signals/slop.ts`, byte-parity-verified against the live gate's own copy). -export function runSlopAssessment(input) { - return buildSlopAssessment(input); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xvcC1hc3Nlc3NtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic2xvcC1hc3Nlc3NtZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBR3ZELG1HQUFtRztBQUNuRywyR0FBMkc7QUFDM0csbUZBQW1GO0FBQ25GLGtIQUFrSDtBQUNsSCw4R0FBOEc7QUFDOUcsc0dBQXNHO0FBQ3RHLCtHQUErRztBQUMvRywyR0FBMkc7QUFDM0csaUdBQWlHO0FBQ2pHLDBHQUEwRztBQUUxRyxNQUFNLFVBQVUsaUJBQWlCLENBQUMsS0FBMEI7SUFDMUQsT0FBTyxtQkFBbUIsQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/stack-detection.d.ts b/packages/loopover-miner/lib/stack-detection.d.ts deleted file mode 100644 index 4ee25696d9..0000000000 --- a/packages/loopover-miner/lib/stack-detection.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** Which manifest (and lockfile, when present) drove the detection. */ -export type StackEvidence = { - manifest: string; - lockfile: string | null; -}; -/** A confidently-detected stack. Command fields are `null` when the command can't be inferred without guessing. */ -export type DetectedRepoStack = { - detected: true; - language: string; - packageManager: string | null; - buildCommand: string | null; - testCommand: string | null; - lintCommand: string | null; - formatCommand: string | null; - evidence: StackEvidence; -}; -/** A repo whose stack could not be confidently identified. */ -export type UndetectedRepoStack = { - detected: false; - reason: string; -}; -export type RepoStackResult = DetectedRepoStack | UndetectedRepoStack; -export type DetectRepoStackOptions = { - existsSync?: (path: string) => boolean; - readFileSync?: (path: string, encoding: "utf8") => string; -}; -/** Manifests, in the precedence order detection tries them; the first matching primary manifest wins. A caller with - * a known polyglot repo can inspect `evidence.manifest` to see which one was chosen. */ -export declare const RECOGNIZED_MANIFESTS: readonly string[]; -/** - * Detect the stack of an already-cloned repository at `repoPath`. Returns `{ detected: true, ... }` with the - * language, package manager, and any confidently-inferred commands, or `{ detected: false, reason }` when no - * recognized manifest is present. Never throws. - */ -export declare function detectRepoStack(repoPath: string, options?: DetectRepoStackOptions): RepoStackResult; -/** One-line human summary of a detection result, suitable for a coding-agent prompt or an operator log. */ -export declare function renderStackSummary(stack: RepoStackResult): string; diff --git a/packages/loopover-miner/lib/stack-detection.js b/packages/loopover-miner/lib/stack-detection.js deleted file mode 100644 index b22ed3baf4..0000000000 --- a/packages/loopover-miner/lib/stack-detection.js +++ /dev/null @@ -1,240 +0,0 @@ -/** Stack auto-detection (#4785): inspect an already-cloned target repo's manifest / lockfile / config files and - * infer a structured description of its stack — language, package manager, and the build / test / lint / format - * commands — before any code-generation step runs. Like `miner-goal-spec.js` this reads the ALREADY-CLONED repo on - * disk (attempt-worktree.js's prepareAttemptWorktree runs first), so the injected `existsSync` / `readFileSync` - * always receive the FULL joined path, mirroring node:fs. It is pure and NEVER throws: an unreadable/unparseable - * file degrades to "no evidence" rather than crashing, and — per the acceptance criteria — a repo whose stack - * can't be confidently identified returns an explicit `{ detected: false, reason }` instead of guessing. */ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -/** Manifests, in the precedence order detection tries them; the first matching primary manifest wins. A caller with - * a known polyglot repo can inspect `evidence.manifest` to see which one was chosen. */ -export const RECOGNIZED_MANIFESTS = Object.freeze([ - "package.json", - "pyproject.toml", - "setup.py", - "setup.cfg", - "requirements.txt", - "Pipfile", - "Cargo.toml", - "go.mod", - "pom.xml", - "build.gradle", - "build.gradle.kts", -]); -const NO_MANIFEST_REASON = "No recognized dependency manifest (package.json, pyproject.toml, Cargo.toml, go.mod, pom.xml, or build.gradle) was found at the repository root."; -const NODE_PACKAGE_MANAGERS = Object.freeze(["npm", "yarn", "pnpm", "bun"]); -const NODE_LOCKFILES = Object.freeze([ - ["pnpm-lock.yaml", "pnpm"], - ["yarn.lock", "yarn"], - ["bun.lockb", "bun"], - ["package-lock.json", "npm"], -]); -/** Build a never-throwing accessor over the cloned repo. `exists` and `read` both swallow fs errors so the detector - * treats an EACCES/ENOENT/binary file as simply "absent" instead of crashing the attempt. */ -function makeAccess(repoPath, options) { - const existsImpl = options.existsSync ?? existsSync; - const readImpl = options.readFileSync ?? readFileSync; - const exists = (relativePath) => { - try { - return existsImpl(join(repoPath, relativePath)) === true; - } - catch { - return false; - } - }; - const read = (relativePath) => { - try { - if (!exists(relativePath)) - return null; - const content = readImpl(join(repoPath, relativePath), "utf8"); - return typeof content === "string" ? content : null; - } - catch { - return null; - } - }; - return { exists, read }; -} -function parseJson(text) { - if (typeof text !== "string") - return null; - try { - const parsed = JSON.parse(text); - return parsed && typeof parsed === "object" ? parsed : null; - } - catch { - return null; - } -} -/** Pick a package.json script by exact name first, then by pattern, considering only string-valued scripts. */ -function pickScript(scripts, exactName, pattern) { - const names = Object.keys(scripts).filter((name) => typeof scripts[name] === "string"); - if (names.includes(exactName)) - return exactName; - return names.find((name) => pattern.test(name)) ?? null; -} -function nodeLockfile(exists) { - const match = NODE_LOCKFILES.find(([file]) => exists(file)); - return match ? match[0] : null; -} -function nodePackageManager(pkg, lockfile) { - const corepack = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0].trim().toLowerCase() : ""; - if (NODE_PACKAGE_MANAGERS.includes(corepack)) - return corepack; - const byLock = NODE_LOCKFILES.find(([file]) => file === lockfile); - // A package.json with no lockfile is still a Node project; npm is its default runner (a default, not a guess). - return byLock ? byLock[1] : "npm"; -} -function hasTypescriptDependency(pkg) { - const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) }; - return typeof deps.typescript === "string"; -} -function detectNode({ exists, read }) { - if (!exists("package.json")) - return null; - const pkg = parseJson(read("package.json")); - const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts && !Array.isArray(pkg.scripts) ? pkg.scripts : {}; - const language = exists("tsconfig.json") || hasTypescriptDependency(pkg) ? "typescript" : "javascript"; - const lockfile = nodeLockfile(exists); - const packageManager = nodePackageManager(pkg, lockfile); - const buildName = pickScript(scripts, "build", /^(build|compile|bundle)(:|$)/i); - const testName = pickScript(scripts, "test", /(^|:)test(:|$)/i); - const lintName = pickScript(scripts, "lint", /(^|:)lint(:|$)/i); - const formatName = pickScript(scripts, "format", /(^|:)(format|fmt)(:|$)/i); - return { - language, - packageManager, - buildCommand: buildName ? `${packageManager} run ${buildName}` : null, - // ` test` is the built-in test lifecycle across npm/yarn/pnpm/bun; a non-"test" script uses `run`. - testCommand: testName ? (testName === "test" ? `${packageManager} test` : `${packageManager} run ${testName}`) : null, - lintCommand: lintName ? `${packageManager} run ${lintName}` : null, - formatCommand: formatName ? `${packageManager} run ${formatName}` : null, - evidence: { manifest: "package.json", lockfile }, - }; -} -function detectPython({ exists, read }) { - const manifest = ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"].find(exists); - if (manifest === undefined) - return null; - const pyproject = read("pyproject.toml") ?? ""; - let packageManager; - let lockfile = null; - if (exists("poetry.lock") || /\[tool\.poetry\]/.test(pyproject)) { - packageManager = "poetry"; - lockfile = exists("poetry.lock") ? "poetry.lock" : null; - } - else if (exists("uv.lock")) { - packageManager = "uv"; - lockfile = "uv.lock"; - } - else if (exists("Pipfile") || exists("Pipfile.lock")) { - packageManager = "pipenv"; - lockfile = exists("Pipfile.lock") ? "Pipfile.lock" : null; - } - else { - packageManager = "pip"; - } - // Commands are inferred only from real config so an undeclared tool is never guessed (acceptance: fail safe). - const hasRuff = exists("ruff.toml") || exists(".ruff.toml") || /\[tool\.ruff\]/.test(pyproject); - const hasPytest = exists("pytest.ini") || exists("tox.ini") || /\[tool\.pytest\b/.test(pyproject); - return { - language: "python", - packageManager, - buildCommand: /\[build-system\]/.test(pyproject) ? (packageManager === "poetry" ? "poetry build" : "python -m build") : null, - testCommand: hasPytest ? "pytest" : null, - lintCommand: hasRuff ? "ruff check ." : null, - formatCommand: hasRuff ? "ruff format ." : null, - evidence: { manifest, lockfile }, - }; -} -function detectRust({ exists }) { - if (!exists("Cargo.toml")) - return null; - return { - language: "rust", - packageManager: "cargo", - buildCommand: "cargo build", - testCommand: "cargo test", - lintCommand: "cargo clippy", - formatCommand: "cargo fmt", - evidence: { manifest: "Cargo.toml", lockfile: exists("Cargo.lock") ? "Cargo.lock" : null }, - }; -} -function detectGo({ exists }) { - if (!exists("go.mod")) - return null; - const hasGolangci = exists(".golangci.yml") || exists(".golangci.yaml") || exists(".golangci.toml"); - return { - language: "go", - packageManager: "go", - buildCommand: "go build ./...", - testCommand: "go test ./...", - lintCommand: hasGolangci ? "golangci-lint run" : "go vet ./...", - formatCommand: "gofmt -l .", - evidence: { manifest: "go.mod", lockfile: exists("go.sum") ? "go.sum" : null }, - }; -} -function detectMaven({ exists }) { - if (!exists("pom.xml")) - return null; - return { - language: "java", - packageManager: "maven", - buildCommand: "mvn -B package", - testCommand: "mvn -B test", - lintCommand: null, - formatCommand: null, - evidence: { manifest: "pom.xml", lockfile: null }, - }; -} -function detectGradle({ exists }) { - const manifest = exists("build.gradle") ? "build.gradle" : exists("build.gradle.kts") ? "build.gradle.kts" : null; - if (manifest === null) - return null; - const runner = exists("gradlew") ? "./gradlew" : "gradle"; - return { - language: "java", - packageManager: "gradle", - buildCommand: `${runner} build`, - testCommand: `${runner} test`, - lintCommand: null, - formatCommand: null, - evidence: { manifest, lockfile: null }, - }; -} -const DETECTORS = Object.freeze([detectNode, detectPython, detectRust, detectGo, detectMaven, detectGradle]); -/** - * Detect the stack of an already-cloned repository at `repoPath`. Returns `{ detected: true, ... }` with the - * language, package manager, and any confidently-inferred commands, or `{ detected: false, reason }` when no - * recognized manifest is present. Never throws. - */ -export function detectRepoStack(repoPath, options = {}) { - if (typeof repoPath !== "string" || !repoPath.trim()) { - return { detected: false, reason: "A repository path is required to detect the stack." }; - } - const access = makeAccess(repoPath, options); - for (const detector of DETECTORS) { - const detected = detector(access); - if (detected !== null) { - // Detectors may leave lockfile undefined; DetectedRepoStack wants string | null (pre-existing shape). - return { detected: true, ...detected, evidence: { ...detected.evidence, lockfile: detected.evidence.lockfile ?? null } }; - } - } - return { detected: false, reason: NO_MANIFEST_REASON }; -} -/** One-line human summary of a detection result, suitable for a coding-agent prompt or an operator log. */ -export function renderStackSummary(stack) { - if (!stack || stack.detected !== true) { - return `stack not detected: ${stack?.reason ?? "unknown reason"}`; - } - const commands = [ - stack.buildCommand ? `build=\`${stack.buildCommand}\`` : null, - stack.testCommand ? `test=\`${stack.testCommand}\`` : null, - stack.lintCommand ? `lint=\`${stack.lintCommand}\`` : null, - stack.formatCommand ? `format=\`${stack.formatCommand}\`` : null, - ].filter((entry) => entry !== null); - const suffix = commands.length > 0 ? ` (${commands.join(", ")})` : " (no validation commands detected)"; - return `${stack.language} via ${stack.packageManager ?? "unknown"}${suffix}`; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhY2stZGV0ZWN0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic3RhY2stZGV0ZWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7NEdBTTRHO0FBQzVHLE9BQU8sRUFBRSxVQUFVLEVBQUUsWUFBWSxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ25ELE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFpQ2pDO3dGQUN3RjtBQUN4RixNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hELGNBQWM7SUFDZCxnQkFBZ0I7SUFDaEIsVUFBVTtJQUNWLFdBQVc7SUFDWCxrQkFBa0I7SUFDbEIsU0FBUztJQUNULFlBQVk7SUFDWixRQUFRO0lBQ1IsU0FBUztJQUNULGNBQWM7SUFDZCxrQkFBa0I7Q0FDbkIsQ0FBQyxDQUFDO0FBRUgsTUFBTSxrQkFBa0IsR0FDdEIsa0pBQWtKLENBQUM7QUFFckosTUFBTSxxQkFBcUIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQztBQUM1RSxNQUFNLGNBQWMsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ25DLENBQUMsZ0JBQWdCLEVBQUUsTUFBTSxDQUFDO0lBQzFCLENBQUMsV0FBVyxFQUFFLE1BQU0sQ0FBQztJQUNyQixDQUFDLFdBQVcsRUFBRSxLQUFLLENBQUM7SUFDcEIsQ0FBQyxtQkFBbUIsRUFBRSxLQUFLLENBQUM7Q0FDN0IsQ0FBQyxDQUFDO0FBRUg7NkZBQzZGO0FBQzdGLFNBQVMsVUFBVSxDQUFDLFFBQWEsRUFBRSxPQUFZO0lBQzdDLE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLElBQUksVUFBVSxDQUFDO0lBQ3BELE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxZQUFZLElBQUksWUFBWSxDQUFDO0lBQ3RELE1BQU0sTUFBTSxHQUFHLENBQUMsWUFBb0IsRUFBRSxFQUFFO1FBQ3RDLElBQUksQ0FBQztZQUNILE9BQU8sVUFBVSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsWUFBWSxDQUFDLENBQUMsS0FBSyxJQUFJLENBQUM7UUFDM0QsQ0FBQztRQUFDLE1BQU0sQ0FBQztZQUNQLE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztJQUNILENBQUMsQ0FBQztJQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsWUFBb0IsRUFBRSxFQUFFO1FBQ3BDLElBQUksQ0FBQztZQUNILElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDO2dCQUFFLE9BQU8sSUFBSSxDQUFDO1lBQ3ZDLE1BQU0sT0FBTyxHQUFHLFFBQVEsQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLFlBQVksQ0FBQyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQy9ELE9BQU8sT0FBTyxPQUFPLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUN0RCxDQUFDO1FBQUMsTUFBTSxDQUFDO1lBQ1AsT0FBTyxJQUFJLENBQUM7UUFDZCxDQUFDO0lBQ0gsQ0FBQyxDQUFDO0lBQ0YsT0FBTyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUMxQixDQUFDO0FBRUQsU0FBUyxTQUFTLENBQUMsSUFBUztJQUMxQixJQUFJLE9BQU8sSUFBSSxLQUFLLFFBQVE7UUFBRSxPQUFPLElBQUksQ0FBQztJQUMxQyxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2hDLE9BQU8sTUFBTSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDOUQsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztBQUNILENBQUM7QUFFRCwrR0FBK0c7QUFDL0csU0FBUyxVQUFVLENBQUMsT0FBWSxFQUFFLFNBQWMsRUFBRSxPQUFZO0lBQzVELE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxPQUFPLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxRQUFRLENBQUMsQ0FBQztJQUN2RixJQUFJLEtBQUssQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDaEQsT0FBTyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDO0FBQzFELENBQUM7QUFFRCxTQUFTLFlBQVksQ0FBQyxNQUFXO0lBQy9CLE1BQU0sS0FBSyxHQUFHLGNBQWMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUM1RCxPQUFPLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7QUFDakMsQ0FBQztBQUVELFNBQVMsa0JBQWtCLENBQUMsR0FBUSxFQUFFLFFBQWE7SUFDakQsTUFBTSxRQUFRLEdBQ1osT0FBTyxHQUFHLEVBQUUsY0FBYyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUN2RyxJQUFJLHFCQUFxQixDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUM7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUM5RCxNQUFNLE1BQU0sR0FBRyxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxFQUFFLENBQUMsSUFBSSxLQUFLLFFBQVEsQ0FBQyxDQUFDO0lBQ2xFLCtHQUErRztJQUMvRyxPQUFPLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUM7QUFDcEMsQ0FBQztBQUVELFNBQVMsdUJBQXVCLENBQUMsR0FBUTtJQUN2QyxNQUFNLElBQUksR0FBRyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsWUFBWSxJQUFJLEVBQUUsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsZUFBZSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUM7SUFDL0UsT0FBTyxPQUFPLElBQUksQ0FBQyxVQUFVLEtBQUssUUFBUSxDQUFDO0FBQzdDLENBQUM7QUFFRCxTQUFTLFVBQVUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQU87SUFDdkMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxjQUFjLENBQUM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN6QyxNQUFNLEdBQUcsR0FBRyxTQUFTLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUM7SUFDNUMsTUFBTSxPQUFPLEdBQ1gsR0FBRyxJQUFJLE9BQU8sR0FBRyxDQUFDLE9BQU8sS0FBSyxRQUFRLElBQUksR0FBRyxDQUFDLE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDMUcsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLGVBQWUsQ0FBQyxJQUFJLHVCQUF1QixDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLFlBQVksQ0FBQztJQUN2RyxNQUFNLFFBQVEsR0FBRyxZQUFZLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDdEMsTUFBTSxjQUFjLEdBQUcsa0JBQWtCLENBQUMsR0FBRyxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBRXpELE1BQU0sU0FBUyxHQUFHLFVBQVUsQ0FBQyxPQUFPLEVBQUUsT0FBTyxFQUFFLCtCQUErQixDQUFDLENBQUM7SUFDaEYsTUFBTSxRQUFRLEdBQUcsVUFBVSxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztJQUNoRSxNQUFNLFFBQVEsR0FBRyxVQUFVLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxpQkFBaUIsQ0FBQyxDQUFDO0lBQ2hFLE1BQU0sVUFBVSxHQUFHLFVBQVUsQ0FBQyxPQUFPLEVBQUUsUUFBUSxFQUFFLHlCQUF5QixDQUFDLENBQUM7SUFFNUUsT0FBTztRQUNMLFFBQVE7UUFDUixjQUFjO1FBQ2QsWUFBWSxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUMsR0FBRyxjQUFjLFFBQVEsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUk7UUFDckUsdUdBQXVHO1FBQ3ZHLFdBQVcsRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxLQUFLLE1BQU0sQ0FBQyxDQUFDLENBQUMsR0FBRyxjQUFjLE9BQU8sQ0FBQyxDQUFDLENBQUMsR0FBRyxjQUFjLFFBQVEsUUFBUSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUNySCxXQUFXLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQyxHQUFHLGNBQWMsUUFBUSxRQUFRLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUNsRSxhQUFhLEVBQUUsVUFBVSxDQUFDLENBQUMsQ0FBQyxHQUFHLGNBQWMsUUFBUSxVQUFVLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUN4RSxRQUFRLEVBQUUsRUFBRSxRQUFRLEVBQUUsY0FBYyxFQUFFLFFBQVEsRUFBRTtLQUNqRCxDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBTztJQUN6QyxNQUFNLFFBQVEsR0FBRyxDQUFDLGdCQUFnQixFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsa0JBQWtCLEVBQUUsU0FBUyxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3pHLElBQUksUUFBUSxLQUFLLFNBQVM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUN4QyxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxFQUFFLENBQUM7SUFFL0MsSUFBSSxjQUFjLENBQUM7SUFDbkIsSUFBSSxRQUFRLEdBQUcsSUFBSSxDQUFDO0lBQ3BCLElBQUksTUFBTSxDQUFDLGFBQWEsQ0FBQyxJQUFJLGtCQUFrQixDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO1FBQ2hFLGNBQWMsR0FBRyxRQUFRLENBQUM7UUFDMUIsUUFBUSxHQUFHLE1BQU0sQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUMsYUFBYSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDMUQsQ0FBQztTQUFNLElBQUksTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUM7UUFDN0IsY0FBYyxHQUFHLElBQUksQ0FBQztRQUN0QixRQUFRLEdBQUcsU0FBUyxDQUFDO0lBQ3ZCLENBQUM7U0FBTSxJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsSUFBSSxNQUFNLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQztRQUN2RCxjQUFjLEdBQUcsUUFBUSxDQUFDO1FBQzFCLFFBQVEsR0FBRyxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0lBQzVELENBQUM7U0FBTSxDQUFDO1FBQ04sY0FBYyxHQUFHLEtBQUssQ0FBQztJQUN6QixDQUFDO0lBRUQsOEdBQThHO0lBQzlHLE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxXQUFXLENBQUMsSUFBSSxNQUFNLENBQUMsWUFBWSxDQUFDLElBQUksZ0JBQWdCLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ2hHLE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxZQUFZLENBQUMsSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDLElBQUksa0JBQWtCLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBRWxHLE9BQU87UUFDTCxRQUFRLEVBQUUsUUFBUTtRQUNsQixjQUFjO1FBQ2QsWUFBWSxFQUFFLGtCQUFrQixDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxjQUFjLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLGlCQUFpQixDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUk7UUFDNUgsV0FBVyxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJO1FBQ3hDLFdBQVcsRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsSUFBSTtRQUM1QyxhQUFhLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLElBQUk7UUFDL0MsUUFBUSxFQUFFLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRTtLQUNqQyxDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsVUFBVSxDQUFDLEVBQUUsTUFBTSxFQUFPO0lBQ2pDLElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDdkMsT0FBTztRQUNMLFFBQVEsRUFBRSxNQUFNO1FBQ2hCLGNBQWMsRUFBRSxPQUFPO1FBQ3ZCLFlBQVksRUFBRSxhQUFhO1FBQzNCLFdBQVcsRUFBRSxZQUFZO1FBQ3pCLFdBQVcsRUFBRSxjQUFjO1FBQzNCLGFBQWEsRUFBRSxXQUFXO1FBQzFCLFFBQVEsRUFBRSxFQUFFLFFBQVEsRUFBRSxZQUFZLEVBQUUsUUFBUSxFQUFFLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUU7S0FDM0YsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFTLFFBQVEsQ0FBQyxFQUFFLE1BQU0sRUFBTztJQUMvQixJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ25DLE1BQU0sV0FBVyxHQUFHLE1BQU0sQ0FBQyxlQUFlLENBQUMsSUFBSSxNQUFNLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxNQUFNLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztJQUNwRyxPQUFPO1FBQ0wsUUFBUSxFQUFFLElBQUk7UUFDZCxjQUFjLEVBQUUsSUFBSTtRQUNwQixZQUFZLEVBQUUsZ0JBQWdCO1FBQzlCLFdBQVcsRUFBRSxlQUFlO1FBQzVCLFdBQVcsRUFBRSxXQUFXLENBQUMsQ0FBQyxDQUFDLG1CQUFtQixDQUFDLENBQUMsQ0FBQyxjQUFjO1FBQy9ELGFBQWEsRUFBRSxZQUFZO1FBQzNCLFFBQVEsRUFBRSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUU7S0FDL0UsQ0FBQztBQUNKLENBQUM7QUFFRCxTQUFTLFdBQVcsQ0FBQyxFQUFFLE1BQU0sRUFBTztJQUNsQyxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQ3BDLE9BQU87UUFDTCxRQUFRLEVBQUUsTUFBTTtRQUNoQixjQUFjLEVBQUUsT0FBTztRQUN2QixZQUFZLEVBQUUsZ0JBQWdCO1FBQzlCLFdBQVcsRUFBRSxhQUFhO1FBQzFCLFdBQVcsRUFBRSxJQUFJO1FBQ2pCLGFBQWEsRUFBRSxJQUFJO1FBQ25CLFFBQVEsRUFBRSxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRTtLQUNsRCxDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEVBQUUsTUFBTSxFQUFPO0lBQ25DLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztJQUNsSCxJQUFJLFFBQVEsS0FBSyxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDbkMsTUFBTSxNQUFNLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQztJQUMxRCxPQUFPO1FBQ0wsUUFBUSxFQUFFLE1BQU07UUFDaEIsY0FBYyxFQUFFLFFBQVE7UUFDeEIsWUFBWSxFQUFFLEdBQUcsTUFBTSxRQUFRO1FBQy9CLFdBQVcsRUFBRSxHQUFHLE1BQU0sT0FBTztRQUM3QixXQUFXLEVBQUUsSUFBSTtRQUNqQixhQUFhLEVBQUUsSUFBSTtRQUNuQixRQUFRLEVBQUUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRTtLQUN2QyxDQUFDO0FBQ0osQ0FBQztBQUVELE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxVQUFVLEVBQUUsWUFBWSxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsV0FBVyxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7QUFFN0c7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQUMsUUFBZ0IsRUFBRSxVQUFrQyxFQUFFO0lBQ3BGLElBQUksT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxFQUFFLENBQUM7UUFDckQsT0FBTyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLG9EQUFvRCxFQUFFLENBQUM7SUFDM0YsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFHLFVBQVUsQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDN0MsS0FBSyxNQUFNLFFBQVEsSUFBSSxTQUFTLEVBQUUsQ0FBQztRQUNqQyxNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDbEMsSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7WUFDdEIsc0dBQXNHO1lBQ3RHLE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLEdBQUcsUUFBUSxFQUFFLFFBQVEsRUFBRSxFQUFFLEdBQUcsUUFBUSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUUsUUFBUSxDQUFDLFFBQVEsQ0FBQyxRQUFRLElBQUksSUFBSSxFQUFFLEVBQXVCLENBQUM7UUFDaEosQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsa0JBQWtCLEVBQUUsQ0FBQztBQUN6RCxDQUFDO0FBRUQsMkdBQTJHO0FBQzNHLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxLQUFzQjtJQUN2RCxJQUFJLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEMsT0FBTyx1QkFBdUIsS0FBSyxFQUFFLE1BQU0sSUFBSSxnQkFBZ0IsRUFBRSxDQUFDO0lBQ3BFLENBQUM7SUFDRCxNQUFNLFFBQVEsR0FBRztRQUNmLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLFdBQVcsS0FBSyxDQUFDLFlBQVksSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJO1FBQzdELEtBQUssQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLFVBQVUsS0FBSyxDQUFDLFdBQVcsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJO1FBQzFELEtBQUssQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLFVBQVUsS0FBSyxDQUFDLFdBQVcsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJO1FBQzFELEtBQUssQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLFlBQVksS0FBSyxDQUFDLGFBQWEsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJO0tBQ2pFLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLEtBQUssSUFBSSxDQUFDLENBQUM7SUFDcEMsTUFBTSxNQUFNLEdBQUcsUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxvQ0FBb0MsQ0FBQztJQUN4RyxPQUFPLEdBQUcsS0FBSyxDQUFDLFFBQVEsUUFBUSxLQUFLLENBQUMsY0FBYyxJQUFJLFNBQVMsR0FBRyxNQUFNLEVBQUUsQ0FBQztBQUMvRSxDQUFDIn0= \ No newline at end of file diff --git a/packages/loopover-miner/lib/status.d.ts b/packages/loopover-miner/lib/status.d.ts deleted file mode 100644 index 1a1d9c0786..0000000000 --- a/packages/loopover-miner/lib/status.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -export type MinerDriverStatus = { - provider: string | null; - modelEnvVar: string | null; - cliPresent: boolean | null; -}; -export type MinerStatus = { - package: { - name: string; - version: string | null; - }; - engine: { - name: string; - version: string | null; - }; - node: string; - stateDir: string; - configFile: string | null; - driver: MinerDriverStatus; -}; -export type DoctorCheck = { - name: string; - ok: boolean; - detail: string; -}; -/** The miner's local-state directory (holds the run-state / queue / ledger SQLite files). */ -export declare function resolveMinerStateDir(env?: Record): string; -/** - * The REAL installed @loopover/engine version, for `status`'s own display. Prefers `readInstalled` - * (the actually-resolved semver from node_modules/the monorepo workspace, the same real resolution `doctor`'s - * engine-version-skew check already relies on) -- a self-hoster asking "what's installed" wants the real - * answer, not the declared dependency RANGE ("*" in this monorepo, which tells them nothing). Falls back to - * the declared range only if real resolution genuinely comes up empty (the engine package's `exports` map - * blocks `require("/package.json")` in some resolution orders, and its built `dist` may be absent - * depending on build order) -- still better than reporting nothing at all. - * - * Exported + injectable (mirrors `buildEngineVersionSkewCheck`'s own `readInstalled` param): real resolution - * succeeding is the only realistic case in a working install, so the fallback path needs a way to force it. - */ -export declare function buildEngineVersionDisplay(readInstalled?: () => string | null): string | null; -export declare function readInstalledEnginePackageVersionFromPaths(resolvedEntry: string, workspacePkg: string, deps?: { - existsSync: (path: string) => boolean; - readFileSync: (path: string, encoding: "utf8") => string; -}): string | null; -/** Installed @loopover/engine semver from node_modules (not the declared dependency range). */ -export declare function readInstalledEnginePackageVersion(): string | null; -/** Expected minimum engine semver: monorepo engine package.json when present, else the shipped pin file. */ -export declare function readExpectedEnginePackageVersionFromPaths(monorepoEnginePkg: string, pinFile: string, deps?: { - existsSync: (path: string) => boolean; - readFileSync: (path: string, encoding: "utf8") => string; -}): string | null; -export declare function readExpectedEnginePackageVersion(): string | null; -/** Returns -1 when installed is behind expected, 0 when equal, 1 when ahead. */ -export declare function compareInstalledEngineVersion(installed: string, expected: string): -1 | 0 | 1; -export declare function buildEngineVersionSkewCheck(readInstalled?: () => string | null, readExpected?: () => string | null): DoctorCheck; -/** Gather the read-only status snapshot. Pure w.r.t. its (env, cwd) inputs — no writes, no network. */ -export declare function collectStatus(env?: Record, cwd?: string): MinerStatus; -export declare function runStatus(args?: string[], env?: Record, cwd?: string): number; -/** Validate the discovered `.loopover-miner` config's CONTENT (#4873), not just its path: parse it with the - * tolerant goal-spec parser and surface its warnings, so a malformed config is flagged by `doctor` rather than - * silently degrading to defaults. No config file is fine (defaults apply); a read failure is reported. `readImpl` - * is injectable for tests. */ -export declare function checkConfigContent(cwd: string, readImpl?: (path: string, encoding: "utf8") => string): DoctorCheck; -/** GitHub token presence (#5170, extended by #6116). A purely offline check — `doctor` never calls GitHub — but - * a missing token fails every real attempt the moment it tries to push a branch or open a PR, so surface it up - * front rather than mid-run. Checks BOTH a GITHUB_TOKEN env override AND a recorded `loopover-mcp login` - * session (hasGitHubTokenSource, offline: reads the local config file, makes no network call) -- otherwise a - * user who only ran `loopover-mcp login` (the new primary flow) would see a spurious "not set" warning even - * though AMS would resolve a live token from that session at attempt time. A session recorded here is not - * re-verified as still valid/unexpired -- only an actual attempt (or resolveGitHubToken itself) discovers - * that. Reports presence only; no token value is ever included in the detail. */ -export declare function checkGitHubTokenPresent(env?: Record): DoctorCheck; -/** Credential presence for the CONFIGURED coding-agent provider (#5170). Distinct from the CLI-present checks, - * which by design keep `ok: true` when only the credential is missing (#5165): this FAILS `doctor` when the - * resolved provider's credential is absent, so an operator learns before an attempt fails partway through. - * Fully offline — an env-var string check for the Claude backends, a file-readability check for codex — and it - * never prints the credential value, only the env-var names / file path. `resolveAuthPath` is injectable for - * tests, mirroring `checkCodexCliPresent`. */ -export declare function checkCodingAgentCredential(env?: Record, resolveAuthPath?: (env: Record) => string): DoctorCheck; -/** Run the doctor checks. Returns an array of { name, ok, detail }; only writes a transient probe in the state dir, - * never touches the network. */ -export declare function runDoctorChecks(env?: Record, cwd?: string): DoctorCheck[]; -export declare function runDoctor(args?: string[], env?: Record, cwd?: string): number; diff --git a/packages/loopover-miner/lib/status.js b/packages/loopover-miner/lib/status.js deleted file mode 100644 index 1f8f5dfb0d..0000000000 --- a/packages/loopover-miner/lib/status.js +++ /dev/null @@ -1,448 +0,0 @@ -import { accessSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { homedir } from "node:os"; -import { join } from "node:path"; -import { CODING_AGENT_DRIVER_CONFIG_ENV, parseMinerGoalSpecContent, resolveFirstConfiguredCodingAgentDriverName } from "@loopover/engine"; -import { checkClaudeCliPresent, checkCodexCliPresent, checkDockerPresent, checkLaptopStateSqlite, findExecutableOnPath, resolveCodexAuthPath, } from "./laptop-init.js"; -import { resolveMinerVersion } from "./version.js"; -import { checkStoreIntegrity, describeError } from "./store-maintenance.js"; -import { resolveEventLedgerDbPath } from "./event-ledger.js"; -import { resolveGovernorLedgerDbPath } from "./governor-ledger.js"; -import { hasGitHubTokenSource } from "./github-token-resolution.js"; -import { resolvePredictionLedgerDbPath } from "./prediction-ledger.js"; -import { resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; -import { resolveClaimLedgerDbPath } from "./claim-ledger.js"; -import { resolveRunStateDbPath } from "./run-state.js"; -import { resolvePlanStoreDbPath } from "./plan-store.js"; -import { resolveGovernorStateDbPath } from "./governor-state.js"; -import { resolveAttemptLogDbPath } from "./attempt-log.js"; -import { resolveReplaySnapshotDbPath } from "./replay-snapshot.js"; -import { resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js"; -import { resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; -import { resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; -import { resolvePolicyDocCacheDbPath } from "./policy-doc-cache.js"; -// Slim laptop-mode CLI commands (#2288): `status` (what's installed + where local state lives) and `doctor` (is -// this laptop set up correctly). Both are read-only and 100% local — no repo-scanning, no coding-agent invocation, -// no GitHub writes, and no network calls of any kind. Later phases add the real discover/plan/manage loop. -// Lazy, not module-scope: mirrors the loopover-engine repo-map.ts fix -- this file is CLI-only today, but -// an eager createRequire(import.meta.url)/import.meta.dirname at module scope would crash on import in any -// bundler context where import.meta is unavailable (e.g. if a future import chain pulls this into a Worker -// bundle, the way repo-map.ts was). Deferring construction to first real use keeps this import-safe. -let cachedRequire = null; -function requireFromHere() { - return (cachedRequire ??= createRequire(import.meta.url)); -} -let cachedModuleDir = null; -function moduleDir() { - return (cachedModuleDir ??= import.meta.dirname); -} -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). -const CONFIG_FILE_CANDIDATES = Object.freeze([ - ".loopover-miner.yml", - ".github/loopover-miner.yml", - ".loopover-miner.json", - ".github/loopover-miner.json", -]); -/** The miner's local-state directory (holds the run-state / queue / ledger SQLite files). */ -export function resolveMinerStateDir(env = process.env) { - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" ? env.LOOPOVER_MINER_CONFIG_DIR.trim() : ""; - if (explicitConfigDir) - return explicitConfigDir; - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner"); -} -/** - * The REAL installed @loopover/engine version, for `status`'s own display. Prefers `readInstalled` - * (the actually-resolved semver from node_modules/the monorepo workspace, the same real resolution `doctor`'s - * engine-version-skew check already relies on) -- a self-hoster asking "what's installed" wants the real - * answer, not the declared dependency RANGE ("*" in this monorepo, which tells them nothing). Falls back to - * the declared range only if real resolution genuinely comes up empty (the engine package's `exports` map - * blocks `require("/package.json")` in some resolution orders, and its built `dist` may be absent - * depending on build order) -- still better than reporting nothing at all. - * - * Exported + injectable (mirrors `buildEngineVersionSkewCheck`'s own `readInstalled` param): real resolution - * succeeding is the only realistic case in a working install, so the fallback path needs a way to force it. - */ -export function buildEngineVersionDisplay(readInstalled = readInstalledEnginePackageVersion) { - const installed = readInstalled(); - if (installed) - return installed; - try { - /* v8 ignore next -- package.json declares @loopover/engine in every supported miner build */ - return requireFromHere()("../package.json").dependencies?.[ENGINE_PACKAGE] ?? null; - } - catch { - /* v8 ignore next -- import metadata/package resolution failure is bundler-only; normal Node tests resolve it */ - return null; - } -} -function readEngineVersion() { - return buildEngineVersionDisplay(); -} -export function readInstalledEnginePackageVersionFromPaths(resolvedEntry, workspacePkg, deps = { - existsSync, - readFileSync, -}) { - try { - for (const pkgJson of [join(resolvedEntry, "..", "package.json"), join(resolvedEntry, "..", "..", "package.json")]) { - if (deps.existsSync(pkgJson)) { - const version = JSON.parse(deps.readFileSync(pkgJson, "utf8")).version; - if (version) - return version; - } - } - } - catch { - // fall through to monorepo workspace fallback - } - if (deps.existsSync(workspacePkg)) { - try { - return JSON.parse(deps.readFileSync(workspacePkg, "utf8")).version ?? null; - } - catch { - return null; - } - } - return null; -} -/** Installed @loopover/engine semver from node_modules (not the declared dependency range). */ -/* v8 ignore next -- Node resolver failure cannot be induced after this module's require is initialized; fallback is defensive */ -export function readInstalledEnginePackageVersion() { - try { - return readInstalledEnginePackageVersionFromPaths(requireFromHere().resolve(ENGINE_PACKAGE), join(moduleDir(), "../../loopover-engine/package.json")); - } - catch { - /* v8 ignore next 9 -- only reaches when Node cannot resolve the installed package at all */ - const workspacePkg = join(moduleDir(), "../../loopover-engine/package.json"); - if (existsSync(workspacePkg)) { - try { - return JSON.parse(readFileSync(workspacePkg, "utf8")).version ?? null; - } - catch { - return null; - } - } - return null; - } -} -/** Expected minimum engine semver: monorepo engine package.json when present, else the shipped pin file. */ -export function readExpectedEnginePackageVersionFromPaths(monorepoEnginePkg, pinFile, deps = { - existsSync, - readFileSync, -}) { - if (deps.existsSync(monorepoEnginePkg)) { - try { - return JSON.parse(deps.readFileSync(monorepoEnginePkg, "utf8")).version ?? null; - } - catch { - return null; - } - } - try { - const pinned = deps.readFileSync(pinFile, "utf8").trim(); - return pinned || null; - } - catch { - return null; - } -} -export function readExpectedEnginePackageVersion() { - return readExpectedEnginePackageVersionFromPaths(join(moduleDir(), "../../loopover-engine/package.json"), join(moduleDir(), "../expected-engine.version")); -} -function parseSemverCore(version) { - const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)/); - if (!match) - return null; - return [Number(match[1]), Number(match[2]), Number(match[3])]; -} -/** Returns -1 when installed is behind expected, 0 when equal, 1 when ahead. */ -export function compareInstalledEngineVersion(installed, expected) { - const installedCore = parseSemverCore(installed); - const expectedCore = parseSemverCore(expected); - if (!installedCore || !expectedCore) - return -1; - for (let index = 0; index < 3; index += 1) { - if (installedCore[index] < expectedCore[index]) - return -1; - if (installedCore[index] > expectedCore[index]) - return 1; - } - return 0; -} -export function buildEngineVersionSkewCheck(readInstalled = readInstalledEnginePackageVersion, readExpected = readExpectedEnginePackageVersion) { - const installed = readInstalled(); - const expected = readExpected(); - if (!expected) { - return { name: "engine-version-skew", ok: true, detail: "expected engine version unavailable (skipped)" }; - } - if (!installed) { - return { - name: "engine-version-skew", - ok: false, - detail: `${ENGINE_PACKAGE} not installed (cannot verify version skew)`, - }; - } - const comparison = compareInstalledEngineVersion(installed, expected); - return { - name: "engine-version-skew", - ok: comparison >= 0, - detail: comparison < 0 - ? `installed ${installed} is behind expected ${expected}` - : `installed ${installed} (${comparison === 0 ? "matches" : "ahead of"} expected ${expected})`, - }; -} -function checkEngineVersionSkew() { - return buildEngineVersionSkewCheck(); -} -/** The minimum Node major version from the package's `engines.node` floor (e.g. ">=22.13.0" → 22). */ -function requiredNodeMajor() { - const engines = requireFromHere()("../package.json").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 */ - return match ? Number(match[1]) : 0; -} -function discoverConfigFile(cwd) { - for (const candidate of CONFIG_FILE_CANDIDATES) { - const path = join(cwd, candidate); - if (existsSync(path)) - return path; - } - return null; -} -// CLI names driver-factory.ts's resolved provider values that actually spawn a local subprocess -- "noop" and -// "agent-sdk" have no separate CLI binary to check presence for, so cliPresent is null (not applicable) for them. -const PROVIDER_CLI_BINARY = Object.freeze({ "claude-cli": "claude", "codex-cli": "codex" }); -/** The `driver` section of `status`/`status --json` (#5164): which coding-agent provider is configured, the - * NAME (never the value) of its model env var, and whether its CLI binary is on PATH. Reuses - * `resolveFirstConfiguredCodingAgentDriverName`/`CODING_AGENT_DRIVER_CONFIG_ENV` (the same resolution - * driver-factory.ts uses) and `findExecutableOnPath` (the same PATH scan the doctor CLI-presence checks use) - * rather than duplicating either. Never reads or returns an env var's actual value. */ -function resolveDriverStatus(env) { - const provider = resolveFirstConfiguredCodingAgentDriverName(env) ?? null; - /* v8 ignore next -- resolved provider names always have a static driver configuration */ - const driverConfig = provider - ? (CODING_AGENT_DRIVER_CONFIG_ENV[provider] ?? null) - : null; - const modelEnvVar = driverConfig?.model ?? null; - const cliBinary = provider ? (PROVIDER_CLI_BINARY[provider] ?? null) : null; - const cliPresent = cliBinary ? Boolean(findExecutableOnPath(cliBinary, env)) : null; - return { provider, modelEnvVar, cliPresent }; -} -/** Gather the read-only status snapshot. Pure w.r.t. its (env, cwd) inputs — no writes, no network. */ -export function collectStatus(env = process.env, cwd = process.cwd()) { - const stateDir = resolveMinerStateDir(env); - return { - package: { name: PACKAGE_NAME, version: resolveMinerVersion(env) }, - engine: { name: ENGINE_PACKAGE, version: readEngineVersion() }, - node: process.version, - stateDir, - configFile: discoverConfigFile(cwd), - driver: resolveDriverStatus(env), - }; -} -function renderDriverLine(driver) { - if (!driver.provider) - return "driver: none configured"; - const cliText = driver.cliPresent === null ? "n/a" : driver.cliPresent ? "yes" : "no"; - const modelText = driver.modelEnvVar ? `, model env: ${driver.modelEnvVar}` : ""; - return `driver: ${driver.provider} (CLI present: ${cliText}${modelText})`; -} -function renderStatusText(status) { - return [ - /* v8 ignore next -- resolveMinerVersion always supplies package.json's build version */ - `${status.package.name} ${status.package.version ?? "unknown"} (node ${status.node})`, - /* v8 ignore next -- engine fallback is already handled in buildEngineVersionDisplay */ - `engine: ${status.engine.name} ${status.engine.version ?? "unresolved"}`, - `state dir: ${status.stateDir}`, - `config file: ${status.configFile ?? "none found"}`, - renderDriverLine(status.driver), - ].join("\n"); -} -export function runStatus(args = [], env = process.env, cwd = process.cwd()) { - const status = collectStatus(env, cwd); - console.log(args.includes("--json") ? JSON.stringify(status, null, 2) : renderStatusText(status)); - return 0; -} -function checkStateDirWritable(stateDir) { - const probe = join(stateDir, ".loopover-miner-write-probe"); - try { - // Creating the dir and writing (then removing) a probe file proves it is writable — the state dir must be - // creatable/writable for the local SQLite stores to work. - mkdirSync(stateDir, { recursive: true, mode: 0o700 }); - writeFileSync(probe, ""); - rmSync(probe, { force: true }); - return { name: "state-dir-writable", ok: true, detail: stateDir }; - } - catch (error) { - return { - name: "state-dir-writable", - ok: false, - /* v8 ignore next -- Node fs throws Error objects; fallback guards hostile/non-Node implementations */ - detail: `${stateDir}: ${error instanceof Error ? error.message : "not writable"}`, - }; - } -} -/** Per-store `PRAGMA integrity_check` sweep for `doctor` (#4834) — flags a corrupted store instead of probing - * only one with `SELECT 1`. A store file that does not exist yet is healthy by absence. Keep in sync with - * migrate-cli.js's `STORES` list (#6768): every durable local SQLite store using resolveLocalStoreDbPath. */ -function storeIntegrityChecks(env) { - const stores = [ - ["event-ledger", resolveEventLedgerDbPath(env)], - ["governor-ledger", resolveGovernorLedgerDbPath(env)], - ["prediction-ledger", resolvePredictionLedgerDbPath(env)], - ["portfolio-queue", resolvePortfolioQueueDbPath(env)], - ["claim-ledger", resolveClaimLedgerDbPath(env)], - ["run-state", resolveRunStateDbPath(env)], - ["plan-store", resolvePlanStoreDbPath(env)], - ["governor-state", resolveGovernorStateDbPath(env)], - ["attempt-log", resolveAttemptLogDbPath(env)], - // replay-snapshot's .d.ts still types env as ProcessEnv (not yet migrated); cast is lossless. - ["replay-snapshot", resolveReplaySnapshotDbPath(env)], - ["worktree-allocator", resolveWorktreeAllocatorDbPath(env)], - ["contribution-profile", resolveContributionProfileCacheDbPath(env)], - ["policy-verdict-cache", resolvePolicyVerdictCacheDbPath(env)], - ["policy-doc-cache", resolvePolicyDocCacheDbPath(env)], - ]; - return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath)); -} -/** Validate the discovered `.loopover-miner` config's CONTENT (#4873), not just its path: parse it with the - * tolerant goal-spec parser and surface its warnings, so a malformed config is flagged by `doctor` rather than - * silently degrading to defaults. No config file is fine (defaults apply); a read failure is reported. `readImpl` - * is injectable for tests. */ -export function checkConfigContent(cwd, readImpl = readFileSync) { - const configPath = discoverConfigFile(cwd); - if (!configPath) { - return { name: "config-content", ok: true, detail: "no .loopover-miner config found (using defaults)" }; - } - let warnings; - try { - warnings = parseMinerGoalSpecContent(readImpl(configPath, "utf8")).warnings; - } - catch (error) { - return { name: "config-content", ok: false, detail: `${configPath}: ${describeError(error)}` }; - } - return warnings.length === 0 - ? { name: "config-content", ok: true, detail: `${configPath}: valid` } - : { name: "config-content", ok: false, detail: `${configPath}: ${warnings.join("; ")}` }; -} -function nonEmptyEnv(value) { - return typeof value === "string" && value.length > 0; -} -/** GitHub token presence (#5170, extended by #6116). A purely offline check — `doctor` never calls GitHub — but - * a missing token fails every real attempt the moment it tries to push a branch or open a PR, so surface it up - * front rather than mid-run. Checks BOTH a GITHUB_TOKEN env override AND a recorded `loopover-mcp login` - * session (hasGitHubTokenSource, offline: reads the local config file, makes no network call) -- otherwise a - * user who only ran `loopover-mcp login` (the new primary flow) would see a spurious "not set" warning even - * though AMS would resolve a live token from that session at attempt time. A session recorded here is not - * re-verified as still valid/unexpired -- only an actual attempt (or resolveGitHubToken itself) discovers - * that. Reports presence only; no token value is ever included in the detail. */ -export function checkGitHubTokenPresent(env = process.env) { - // hasGitHubTokenSource's .d.ts still types env as ProcessEnv; cast is lossless. - const present = hasGitHubTokenSource(env); - return { - name: "github-token", - ok: present, - detail: present - ? "A GitHub token is available (GITHUB_TOKEN or a loopover-mcp login session)" - : "No GitHub token available — run `loopover-mcp login`, or set GITHUB_TOKEN, before attempts that push a branch or open a PR", - }; -} -/** Credential presence for the CONFIGURED coding-agent provider (#5170). Distinct from the CLI-present checks, - * which by design keep `ok: true` when only the credential is missing (#5165): this FAILS `doctor` when the - * resolved provider's credential is absent, so an operator learns before an attempt fails partway through. - * Fully offline — an env-var string check for the Claude backends, a file-readability check for codex — and it - * never prints the credential value, only the env-var names / file path. `resolveAuthPath` is injectable for - * tests, mirroring `checkCodexCliPresent`. */ -export function checkCodingAgentCredential(env = process.env, resolveAuthPath = resolveCodexAuthPath) { - const provider = resolveFirstConfiguredCodingAgentDriverName(env) ?? null; - if (provider === null || provider === "noop") { - return { - name: "coding-agent-credential", - ok: true, - detail: provider === "noop" - ? "noop driver needs no credential" - : "no coding-agent provider configured (skipped)", - }; - } - if (provider === "claude-cli" || provider === "agent-sdk") { - // Both run the Claude backend (a `claude` subprocess vs the in-process Agent SDK) off the same subscription - // OAuth token the rest of the tree reads (CLAUDE_CODE_OAUTH_TOKEN; see createClaudeCodeAi in - // src/selfhost/ai.ts). The SDK additionally accepts a raw ANTHROPIC_API_KEY, so either satisfies the credential. - const present = nonEmptyEnv(env.CLAUDE_CODE_OAUTH_TOKEN) || nonEmptyEnv(env.ANTHROPIC_API_KEY); - return { - name: "coding-agent-credential", - ok: present, - detail: present - ? `${provider}: Claude credential is set` - : `${provider}: no Claude credential — set CLAUDE_CODE_OAUTH_TOKEN (or ANTHROPIC_API_KEY)`, - }; - } - // codex-cli: the only remaining configured provider — its credential is a readable auth.json, the same - // read-only condition checkCodexCliPresent probes (reusing resolveCodexAuthPath so the location never drifts). - const authPath = resolveAuthPath(env); - let readable = false; - try { - accessSync(authPath, constants.R_OK); - readable = true; - } - catch { - // missing or unreadable — codex would fail for lack of credentials at attempt time. - } - return { - name: "coding-agent-credential", - ok: readable, - detail: readable - ? `codex-cli: auth.json is readable at ${authPath}` - : `codex-cli: auth.json missing or unreadable at ${authPath} — run \`codex auth\``, - }; -} -/** Run the doctor checks. Returns an array of { name, ok, detail }; only writes a transient probe in the state dir, - * never touches the network. */ -export function runDoctorChecks(env = process.env, cwd = process.cwd()) { - const nodeMajor = Number(process.versions.node.split(".")[0]); - const requiredMajor = requiredNodeMajor(); - const engineVersion = readEngineVersion(); - return [ - { - name: "node-version", - ok: nodeMajor >= requiredMajor, - detail: `node ${process.version} (requires >= ${requiredMajor})`, - }, - { - name: "engine-resolves", - ok: engineVersion !== null, - /* v8 ignore next -- buildEngineVersionDisplay has a declared-range fallback in supported Node installs */ - detail: engineVersion ? `${ENGINE_PACKAGE} ${engineVersion}` : `${ENGINE_PACKAGE} not resolvable`, - }, - checkEngineVersionSkew(), - checkStateDirWritable(resolveMinerStateDir(env)), - checkLaptopStateSqlite(env), - checkDockerPresent(), - checkClaudeCliPresent({ env }), - checkCodexCliPresent({ env }), - checkGitHubTokenPresent(env), - checkCodingAgentCredential(env), - checkConfigContent(cwd), - ...storeIntegrityChecks(env), - ]; -} -export function runDoctor(args = [], env = process.env, cwd = process.cwd()) { - const checks = runDoctorChecks(env, cwd); - const failed = checks.filter((check) => !check.ok); - if (args.includes("--json")) { - console.log(JSON.stringify({ ok: failed.length === 0, checks }, null, 2)); - } - else { - for (const check of checks) - console.log(`${check.ok ? "ok " : "FAIL"} ${check.name}: ${check.detail}`); - if (failed.length > 0) - console.error(`doctor: ${failed.length} check(s) failed`); - } - return failed.length === 0 ? 0 : 1; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhdHVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic3RhdHVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxVQUFVLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxhQUFhLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDNUcsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUM1QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ2xDLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDakMsT0FBTyxFQUFFLDhCQUE4QixFQUFFLHlCQUF5QixFQUFFLDJDQUEyQyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDMUksT0FBTyxFQUNMLHFCQUFxQixFQUNyQixvQkFBb0IsRUFDcEIsa0JBQWtCLEVBQ2xCLHNCQUFzQixFQUN0QixvQkFBb0IsRUFDcEIsb0JBQW9CLEdBQ3JCLE1BQU0sa0JBQWtCLENBQUM7QUFDMUIsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sY0FBYyxDQUFDO0FBQ25ELE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxhQUFhLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUM1RSxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNuRSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUNwRSxPQUFPLEVBQUUsNkJBQTZCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUN2RSxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNuRSxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM3RCxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxn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QztRQUMzQixrQkFBa0IsRUFBRTtRQUNwQixxQkFBcUIsQ0FBQyxFQUFFLEdBQUcsRUFBRSxDQUFDO1FBQzlCLG9CQUFvQixDQUFDLEVBQUUsR0FBRyxFQUFFLENBQUM7UUFDN0IsdUJBQXVCLENBQUMsR0FBRyxDQUFDO1FBQzVCLDBCQUEwQixDQUFDLEdBQUcsQ0FBQztRQUMvQixrQkFBa0IsQ0FBQyxHQUFHLENBQUM7UUFDdkIsR0FBRyxvQkFBb0IsQ0FBQyxHQUFHLENBQUM7S0FDN0IsQ0FBQztBQUNKLENBQUM7QUFFRCxNQUFNLFVBQVUsU0FBUyxDQUN2QixPQUFpQixFQUFFLEVBQ25CLE1BQTBDLE9BQU8sQ0FBQyxHQUFHLEVBQ3JELE1BQWMsT0FBTyxDQUFDLEdBQUcsRUFBRTtJQUUzQixNQUFNLE1BQU0sR0FBRyxlQUFlLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQ3pDLE1BQU0sTUFBTSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ25ELElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1FBQzVCLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxNQUFNLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM1RSxDQUFDO1NBQU0sQ0FBQztRQUNOLEtBQUssTUFBTSxLQUFLLElBQUksTUFBTTtZQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBQ3hHLElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDO1lBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxXQUFXLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixDQUFDLENBQUM7SUFDbkYsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDLE1BQU0sS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3JDLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/store-db-adapter.d.ts b/packages/loopover-miner/lib/store-db-adapter.d.ts deleted file mode 100644 index a25a3b2d90..0000000000 --- a/packages/loopover-miner/lib/store-db-adapter.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { DatabaseSync } from "node:sqlite"; -/** Sync SQLite primitive both node:sqlite and (later) Postgres-backed drivers satisfy (#7175). */ -export interface SqliteDriver { - query(sql: string, params: unknown[]): { - rows: Record[]; - changes: number; - lastInsertRowid: number; - }; - exec(sql: string): void; -} -/** Minimal D1-shaped surface returned by `createD1Adapter` (async wrappers over SqliteDriver). */ -export interface MinerD1Database { - prepare(sql: string): MinerD1PreparedStatement; - batch(statements: MinerD1PreparedStatement[]): Promise; - exec(sql: string): Promise<{ - count: number; - duration: number; - }>; - dump(): Promise; -} -export interface MinerD1PreparedStatement { - bind(...values: unknown[]): MinerD1PreparedStatement; - all(): Promise<{ - results: T[]; - success: true; - meta: Record; - }>; - run(): Promise<{ - results: T[]; - success: true; - meta: Record; - }>; - first(colName?: string): Promise; - raw(): Promise; -} -/** - * Wrap a synchronous SqliteDriver as a D1-shaped database (async prepare/batch/exec). - */ -export declare function createD1Adapter(driver: SqliteDriver): MinerD1Database; -/** - * Build a SqliteDriver from a node:sqlite DatabaseSync. - * A statement with zero result columns is a WRITE; otherwise a READ. - * - * LIMITATION (#7175 follow-up): `INSERT/UPDATE/DELETE … RETURNING` statements report result columns, so - * this heuristic would treat them as reads and drop `changes`/`lastInsertRowid`. claim-ledger and other - * RETURNING callers must not migrate onto `driver.query` until the heuristic is sharpened (e.g. statement - * class detection) or those stores use `createD1Adapter`/`run` exclusively. - */ -export declare function nodeSqliteDriver(db: DatabaseSync): SqliteDriver; diff --git a/packages/loopover-miner/lib/store-db-adapter.js b/packages/loopover-miner/lib/store-db-adapter.js deleted file mode 100644 index 11ff311e3c..0000000000 --- a/packages/loopover-miner/lib/store-db-adapter.js +++ /dev/null @@ -1,106 +0,0 @@ -// Shared SqliteDriver / D1 adapter seam for AMS local stores (#7175 part 1). -// -// Mirrors ORB's `src/selfhost/d1-adapter.ts` so hosted AMS can later swap in `createPgAdapter` without -// inventing a second abstraction. Self-host default remains node:sqlite via `nodeSqliteDriver`. -// Keep this surface in sync with the ORB module when either side grows (Postgres interactive txn / -// `runOn` arrives in a later #7175 slice — not this file yet). -function meta(changes = 0, lastRowId = 0) { - return { - duration: 0, - size_after: 0, - rows_read: 0, - rows_written: changes, - last_row_id: lastRowId, - changed_db: changes > 0, - changes, - }; -} -/** One prepared (and optionally bound) statement — D1 statements are immutable after bind. */ -class Statement { - constructor(driver, sql, values = []) { - this.driver = driver; - this.sql = sql; - this.values = values; - } - bind(...values) { - return new Statement(this.driver, this.sql, values); - } - execSync() { - const r = this.driver.query(this.sql, this.values); - return { results: r.rows, success: true, meta: meta(r.changes, r.lastInsertRowid) }; - } - async all() { - return this.execSync(); - } - async run() { - return this.execSync(); - } - async first(colName) { - const row = this.driver.query(this.sql, this.values).rows[0]; - if (row == null) - return null; - return ((colName != null ? row[colName] : row) ?? null); - } - async raw() { - return this.driver.query(this.sql, this.values).rows.map((row) => Object.values(row)); - } -} -/** - * Wrap a synchronous SqliteDriver as a D1-shaped database (async prepare/batch/exec). - */ -export function createD1Adapter(driver) { - return { - prepare(sql) { - return new Statement(driver, sql); - }, - async batch(statements) { - driver.exec("BEGIN"); - try { - const out = statements.map((s) => s.execSync()); - driver.exec("COMMIT"); - return out; - } - catch (error) { - try { - driver.exec("ROLLBACK"); - } - catch { - /* ignore */ - } - throw error; - } - }, - async exec(sql) { - driver.exec(sql); - return { count: (sql.match(/;/g) ?? []).length || 1, duration: 0 }; - }, - async dump() { - return new ArrayBuffer(0); - }, - }; -} -/** - * Build a SqliteDriver from a node:sqlite DatabaseSync. - * A statement with zero result columns is a WRITE; otherwise a READ. - * - * LIMITATION (#7175 follow-up): `INSERT/UPDATE/DELETE … RETURNING` statements report result columns, so - * this heuristic would treat them as reads and drop `changes`/`lastInsertRowid`. claim-ledger and other - * RETURNING callers must not migrate onto `driver.query` until the heuristic is sharpened (e.g. statement - * class detection) or those stores use `createD1Adapter`/`run` exclusively. - */ -export function nodeSqliteDriver(db) { - return { - query(sql, params) { - const stmt = db.prepare(sql); - if (stmt.columns().length > 0) { - return { rows: stmt.all(...params), changes: 0, lastInsertRowid: 0 }; - } - const info = stmt.run(...params); - return { rows: [], changes: Number(info.changes), lastInsertRowid: Number(info.lastInsertRowid) }; - }, - exec(sql) { - db.exec(sql); - }, - }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmUtZGItYWRhcHRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInN0b3JlLWRiLWFkYXB0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsNkVBQTZFO0FBQzdFLEVBQUU7QUFDRix1R0FBdUc7QUFDdkcsZ0dBQWdHO0FBQ2hHLG1HQUFtRztBQUNuRywrREFBK0Q7QUE2Qi9ELFNBQVMsSUFBSSxDQUFDLE9BQU8sR0FBRyxDQUFDLEVBQUUsU0FBUyxHQUFHLENBQUM7SUFDdEMsT0FBTztRQUNMLFFBQVEsRUFBRSxDQUFDO1FBQ1gsVUFBVSxFQUFFLENBQUM7UUFDYixTQUFTLEVBQUUsQ0FBQztRQUNaLFlBQVksRUFBRSxPQUFPO1FBQ3JCLFdBQVcsRUFBRSxTQUFTO1FBQ3RCLFVBQVUsRUFBRSxPQUFPLEdBQUcsQ0FBQztRQUN2QixPQUFPO0tBQ1IsQ0FBQztBQUNKLENBQUM7QUFFRCw4RkFBOEY7QUFDOUYsTUFBTSxTQUFTO0lBS2IsWUFBWSxNQUFvQixFQUFFLEdBQVcsRUFBRSxTQUFvQixFQUFFO1FBQ25FLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDO1FBQ3JCLElBQUksQ0FBQyxHQUFHLEdBQUcsR0FBRyxDQUFDO1FBQ2YsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7SUFDdkIsQ0FBQztJQUVELElBQUksQ0FBQyxHQUFHLE1BQWlCO1FBQ3ZCLE9BQU8sSUFBSSxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3RELENBQUM7SUFFRCxRQUFRO1FBQ04sTUFBTSxDQUFDLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDbkQsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/store-maintenance.d.ts b/packages/loopover-miner/lib/store-maintenance.d.ts deleted file mode 100644 index 8b67a9ad0b..0000000000 --- a/packages/loopover-miner/lib/store-maintenance.d.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { DatabaseSync } from "node:sqlite"; -/** Env opt-ins for ledger retention (unset ⇒ retention disabled). */ -export declare const LEDGER_RETENTION_DAYS_ENV = "LOOPOVER_MINER_LEDGER_RETENTION_DAYS"; -export declare const LEDGER_RETENTION_MAX_ROWS_ENV = "LOOPOVER_MINER_LEDGER_RETENTION_MAX_ROWS"; -export type LedgerRetentionSpec = { - table: string; - timestampColumn: string; - orderColumn: string; -}; -/** Fixed retention specs for the three append-only ledgers. These identifiers are INTERNAL constants — never - * caller/user text — and are validated as plain identifiers before interpolation as defence in depth. */ -export declare const EVENT_LEDGER_RETENTION_SPEC: LedgerRetentionSpec; -export declare const GOVERNOR_LEDGER_RETENTION_SPEC: LedgerRetentionSpec; -export declare const PREDICTION_LEDGER_RETENTION_SPEC: LedgerRetentionSpec; -export type LedgerPurgeSpec = { - table: string; - repoColumn: string; -}; -/** Fixed purge specs (#5564, #6599) for the six stores whose rows are directly scoped by a `repoColumn`. Same - * internal-constant-only discipline as the retention specs above. `attempt-log.js` is deliberately absent: its - * payload is a free-form `Record` with no dedicated repo column, so a precise per-repo purge - * isn't possible there without risking false matches — `purge-cli.js` reports it as not-purgeable instead. */ -export declare const CLAIM_LEDGER_PURGE_SPEC: LedgerPurgeSpec; -export declare const EVENT_LEDGER_PURGE_SPEC: LedgerPurgeSpec; -export declare const GOVERNOR_LEDGER_PURGE_SPEC: LedgerPurgeSpec; -export declare const PREDICTION_LEDGER_PURGE_SPEC: LedgerPurgeSpec; -export declare const PORTFOLIO_QUEUE_PURGE_SPEC: LedgerPurgeSpec; -export declare const RUN_STATE_PURGE_SPEC: LedgerPurgeSpec; -/** Three more repo-scoped stores the original six missed (#7091), same `repoColumn` shape and same internal- - * constant-only discipline. The contribution-profile-cache table name comes from its schema module's own - * `CONTRIBUTION_PROFILE_STORE_TABLE` constant so this spec can't drift from a second hardcoded literal. - * governor-state holds two genuinely repo-scoped tables (reputation history + own submissions); - * `governor_scalar_state` is intentionally excluded — it is a single whole-run scalar row with no repo - * dimension. `governor_reputation_history` is purged on `repo_full_name` alone (its key is composite with - * `api_base_url`), so a right-to-be-forgotten sweep clears the repo across every forge host it was recorded - * against, not just the default one. */ -export declare const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC: LedgerPurgeSpec; -export declare const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC: LedgerPurgeSpec; -export declare const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC: LedgerPurgeSpec; -/** policy-verdict-cache (#6987), another repo-scoped store the earlier sweeps missed. Its `repo_scope TEXT - * PRIMARY KEY` is the per-repo column (a tenant forge host + `owner/repo`), the same `repoColumn` shape and - * internal-constant-only discipline as the specs above. `policy-doc-cache.js` stays out (keyed by URL, no repo - * column, exactly like `attempt-log.js`). */ -export declare const POLICY_VERDICT_CACHE_PURGE_SPEC: LedgerPurgeSpec; -export type StoreIntegrityResult = { - name: string; - ok: boolean; - detail: string; -}; -export type LedgerRetentionPolicy = { - maxAgeMs?: number; - maxRows?: number; -}; -/** A readable message for a caught value, whether or not it is an Error. */ -export declare function describeError(error: unknown): string; -/** - * Classify raw `PRAGMA integrity_check` rows. A healthy database yields a single `"ok"` row; a corrupt one yields - * one row per problem. Pure — extracted so both the healthy and problem paths are testable without a genuinely - * corrupt file (which SQLite typically refuses to open at all, i.e. the catch path below). - */ -export declare function classifyIntegrityRows(rows: Array<{ - integrity_check?: unknown; -}>): { - ok: boolean; - note: string; -}; -/** - * Run `PRAGMA integrity_check` on a single store file. A store that does not exist yet is healthy by absence - * (nothing to corrupt). Never throws: a store that cannot be opened or read is reported as not-ok, so one bad - * store cannot abort the whole doctor sweep. Opens the connection driver-enforced read-only -- `readOnly` - * (camelCase) is the only option key node:sqlite recognizes for this; the lowercase `readonly` is silently - * ignored and opens read-write instead (the exact gotcha claim-ledger.js's own openClaimLedgerReadOnly already - * documents), which would defeat the read-only guarantee this function's own docs claim. - */ -export declare function checkStoreIntegrity(name: string, dbPath: string): StoreIntegrityResult; -/** - * Resolve the opt-in ledger retention policy from an env object. OFF by default: returns null unless at least - * one bound is set to a positive value. A zero/negative/non-numeric value is treated as unset. When set, returns - * `{ maxAgeMs? }` (from a day count) and/or `{ maxRows? }`. - */ -export declare function resolveLedgerRetentionPolicy(env?: Record): LedgerRetentionPolicy | null; -/** - * Prune one append-only ledger per a resolved retention policy: delete rows older than the age bound AND rows - * beyond the row-count bound (keeping the newest `maxRows` by `orderColumn`), atomically. A null policy is a - * no-op. `nowMs` is caller-supplied (no internal clock). Timestamp columns are UTC ISO-8601 strings, which sort - * lexicographically in chronological order, so a string comparison against the ISO cutoff selects older rows. - */ -export declare function pruneLedgerByRetention(db: DatabaseSync, spec: LedgerRetentionSpec, policy: LedgerRetentionPolicy | null, nowMs: number): number; -/** - * Delete every row for one repo from a store (#5564). Unlike `pruneLedgerByRetention`, this never runs - * automatically — it exists solely so `purge-cli.js` can give an operator a real right-to-be-forgotten path. - * `repoFullName` is caller-normalized (owner/repo) before reaching here; this function only guards the SQL - * identifiers, matching `pruneLedgerByRetention`'s own defence-in-depth discipline. - */ -export declare function purgeStoreByRepo(db: DatabaseSync, spec: LedgerPurgeSpec, repoFullName: string): number; -/** - * Count rows for one repo in a store without deleting anything (#5564) — the read-only counterpart to - * `purgeStoreByRepo`, used by `purge-cli.js --dry-run` to report what a real purge would remove. - */ -export declare function countStoreByRepo(db: DatabaseSync, spec: LedgerPurgeSpec, repoFullName: string): number; diff --git a/packages/loopover-miner/lib/store-maintenance.js b/packages/loopover-miner/lib/store-maintenance.js deleted file mode 100644 index 860991d691..0000000000 --- a/packages/loopover-miner/lib/store-maintenance.js +++ /dev/null @@ -1,180 +0,0 @@ -// Local-store maintenance for the miner (#4834): SQLite integrity checks + append-only ledger retention. -// -// Three independent, side-effect-light helpers used by `doctor`, the ledgers, and `purge-cli.js`: -// 1. checkStoreIntegrity — run `PRAGMA integrity_check` on one store file and report health, so `doctor` can -// flag a corrupted store instead of only probing a single one with `SELECT 1`. -// 2. resolveLedgerRetentionPolicy / pruneLedgerByRetention — an opt-in, age- and/or size-based retention -// policy for the unbounded append-only ledgers (event, governor, prediction), which otherwise grow forever. -// OFF by default: retention only runs when an operator sets the env opt-in. -// 3. purgeStoreByRepo — an explicit, operator-invoked delete of every row for one repo (#5564, right-to-be- -// forgotten). Distinct from retention pruning: never runs automatically, always caller-initiated via -// `purge-cli.js`, and always reports how many rows it removed so a purge is never silent. -// Pure control flow over injected inputs (a DB handle, an env object, a caller-supplied clock) — no network, and -// no internal clock read in the prune path so it stays deterministic and unit-testable. -import { existsSync } from "node:fs"; -import { DatabaseSync } from "node:sqlite"; -import { CONTRIBUTION_PROFILE_STORE_TABLE } from "./contribution-profile.js"; -/** Env opt-ins for ledger retention (unset ⇒ retention disabled). */ -export const LEDGER_RETENTION_DAYS_ENV = "LOOPOVER_MINER_LEDGER_RETENTION_DAYS"; -export const LEDGER_RETENTION_MAX_ROWS_ENV = "LOOPOVER_MINER_LEDGER_RETENTION_MAX_ROWS"; -/** Fixed retention specs for the three append-only ledgers. These identifiers are INTERNAL constants — never - * caller/user text — and are validated as plain identifiers before interpolation as defence in depth. */ -export const EVENT_LEDGER_RETENTION_SPEC = { table: "miner_event_ledger", timestampColumn: "created_at", orderColumn: "id" }; -export const GOVERNOR_LEDGER_RETENTION_SPEC = { table: "governor_events", timestampColumn: "ts", orderColumn: "id" }; -export const PREDICTION_LEDGER_RETENTION_SPEC = { table: "predictions", timestampColumn: "ts", orderColumn: "id" }; -/** Fixed purge specs (#5564, #6599) for the six stores whose rows are directly scoped by a `repoColumn`. Same - * internal-constant-only discipline as the retention specs above. `attempt-log.js` is deliberately absent: its - * payload is a free-form `Record` with no dedicated repo column, so a precise per-repo purge - * isn't possible there without risking false matches — `purge-cli.js` reports it as not-purgeable instead. */ -export const CLAIM_LEDGER_PURGE_SPEC = { table: "miner_claims", repoColumn: "repo_full_name" }; -export const EVENT_LEDGER_PURGE_SPEC = { table: "miner_event_ledger", repoColumn: "repo_full_name" }; -export const GOVERNOR_LEDGER_PURGE_SPEC = { table: "governor_events", repoColumn: "repo_full_name" }; -export const PREDICTION_LEDGER_PURGE_SPEC = { table: "predictions", repoColumn: "repo_full_name" }; -export const PORTFOLIO_QUEUE_PURGE_SPEC = { table: "miner_portfolio_queue", repoColumn: "repo_full_name" }; -export const RUN_STATE_PURGE_SPEC = { table: "miner_run_state", repoColumn: "repo_full_name" }; -/** Three more repo-scoped stores the original six missed (#7091), same `repoColumn` shape and same internal- - * constant-only discipline. The contribution-profile-cache table name comes from its schema module's own - * `CONTRIBUTION_PROFILE_STORE_TABLE` constant so this spec can't drift from a second hardcoded literal. - * governor-state holds two genuinely repo-scoped tables (reputation history + own submissions); - * `governor_scalar_state` is intentionally excluded — it is a single whole-run scalar row with no repo - * dimension. `governor_reputation_history` is purged on `repo_full_name` alone (its key is composite with - * `api_base_url`), so a right-to-be-forgotten sweep clears the repo across every forge host it was recorded - * against, not just the default one. */ -export const CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC = { table: CONTRIBUTION_PROFILE_STORE_TABLE, repoColumn: "repo_full_name" }; -export const GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC = { table: "governor_reputation_history", repoColumn: "repo_full_name" }; -export const GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC = { table: "governor_own_submissions", repoColumn: "repo_full_name" }; -/** policy-verdict-cache (#6987), another repo-scoped store the earlier sweeps missed. Its `repo_scope TEXT - * PRIMARY KEY` is the per-repo column (a tenant forge host + `owner/repo`), the same `repoColumn` shape and - * internal-constant-only discipline as the specs above. `policy-doc-cache.js` stays out (keyed by URL, no repo - * column, exactly like `attempt-log.js`). */ -export const POLICY_VERDICT_CACHE_PURGE_SPEC = { table: "policy_verdict_cache", repoColumn: "repo_scope" }; -const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/; -/** A readable message for a caught value, whether or not it is an Error. */ -export function describeError(error) { - return error instanceof Error ? error.message : String(error); -} -/** - * Classify raw `PRAGMA integrity_check` rows. A healthy database yields a single `"ok"` row; a corrupt one yields - * one row per problem. Pure — extracted so both the healthy and problem paths are testable without a genuinely - * corrupt file (which SQLite typically refuses to open at all, i.e. the catch path below). - */ -export function classifyIntegrityRows(rows) { - const problems = rows.map((row) => String(row.integrity_check)).filter((value) => value !== "ok"); - return problems.length === 0 ? { ok: true, note: "ok" } : { ok: false, note: problems.join("; ") }; -} -/** - * Run `PRAGMA integrity_check` on a single store file. A store that does not exist yet is healthy by absence - * (nothing to corrupt). Never throws: a store that cannot be opened or read is reported as not-ok, so one bad - * store cannot abort the whole doctor sweep. Opens the connection driver-enforced read-only -- `readOnly` - * (camelCase) is the only option key node:sqlite recognizes for this; the lowercase `readonly` is silently - * ignored and opens read-write instead (the exact gotcha claim-ledger.js's own openClaimLedgerReadOnly already - * documents), which would defeat the read-only guarantee this function's own docs claim. - */ -export function checkStoreIntegrity(name, dbPath) { - if (!existsSync(dbPath)) { - return { name, ok: true, detail: `${dbPath}: not created yet` }; - } - let db; - try { - db = new DatabaseSync(dbPath, { readOnly: true }); - const { ok, note } = classifyIntegrityRows(db.prepare("PRAGMA integrity_check").all()); - return { name, ok, detail: `${dbPath}: ${note}` }; - } - catch (error) { - return { name, ok: false, detail: `${dbPath}: ${describeError(error)}` }; - } - finally { - db?.close(); - } -} -/** Coerce an env value to a positive integer, or null (unset/blank/zero/negative/non-finite ⇒ null ⇒ disabled). - * Floors BEFORE the positivity test, so a fractional value below 1 (e.g. "0.5") floors to 0 and disables the - * bound rather than becoming a dangerous 0 that would prune the whole ledger. */ -function positiveIntOrNull(raw) { - if (raw === undefined || raw === null || String(raw).trim() === "") - return null; - const numeric = Math.floor(Number(raw)); - return Number.isFinite(numeric) && numeric > 0 ? numeric : null; -} -/** - * Resolve the opt-in ledger retention policy from an env object. OFF by default: returns null unless at least - * one bound is set to a positive value. A zero/negative/non-numeric value is treated as unset. When set, returns - * `{ maxAgeMs? }` (from a day count) and/or `{ maxRows? }`. - */ -export function resolveLedgerRetentionPolicy(env = process.env) { - const maxAgeDays = positiveIntOrNull(env[LEDGER_RETENTION_DAYS_ENV]); - const maxRows = positiveIntOrNull(env[LEDGER_RETENTION_MAX_ROWS_ENV]); - if (maxAgeDays === null && maxRows === null) - return null; - const policy = {}; - if (maxAgeDays !== null) - policy.maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000; - if (maxRows !== null) - policy.maxRows = maxRows; - return policy; -} -/** - * Prune one append-only ledger per a resolved retention policy: delete rows older than the age bound AND rows - * beyond the row-count bound (keeping the newest `maxRows` by `orderColumn`), atomically. A null policy is a - * no-op. `nowMs` is caller-supplied (no internal clock). Timestamp columns are UTC ISO-8601 strings, which sort - * lexicographically in chronological order, so a string comparison against the ISO cutoff selects older rows. - */ -export function pruneLedgerByRetention(db, spec, policy, nowMs) { - if (!policy) - return 0; - for (const identifier of [spec.table, spec.timestampColumn, spec.orderColumn]) { - if (!SQL_IDENTIFIER.test(identifier)) - throw new Error(`unsafe SQL identifier: ${identifier}`); - } - let deleted = 0; - db.exec("BEGIN"); - try { - // Both bounds are guarded to be strictly positive as defence in depth: a 0 age would prune everything older - // than `now`, and a 0 row-cap makes `LIMIT 0` match no rows so `NOT IN (empty)` would delete the whole ledger. - if (policy.maxAgeMs !== undefined && policy.maxAgeMs > 0) { - const cutoff = new Date(nowMs - policy.maxAgeMs).toISOString(); - const info = db.prepare(`DELETE FROM ${spec.table} WHERE ${spec.timestampColumn} < ?`).run(cutoff); - deleted += Number(info.changes); - } - if (policy.maxRows !== undefined && policy.maxRows >= 1) { - const info = db - .prepare(`DELETE FROM ${spec.table} WHERE ${spec.orderColumn} NOT IN ` + - `(SELECT ${spec.orderColumn} FROM ${spec.table} ORDER BY ${spec.orderColumn} DESC LIMIT ?)`) - .run(policy.maxRows); - deleted += Number(info.changes); - } - db.exec("COMMIT"); - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - return deleted; -} -/** - * Delete every row for one repo from a store (#5564). Unlike `pruneLedgerByRetention`, this never runs - * automatically — it exists solely so `purge-cli.js` can give an operator a real right-to-be-forgotten path. - * `repoFullName` is caller-normalized (owner/repo) before reaching here; this function only guards the SQL - * identifiers, matching `pruneLedgerByRetention`'s own defence-in-depth discipline. - */ -export function purgeStoreByRepo(db, spec, repoFullName) { - for (const identifier of [spec.table, spec.repoColumn]) { - if (!SQL_IDENTIFIER.test(identifier)) - throw new Error(`unsafe SQL identifier: ${identifier}`); - } - const info = db.prepare(`DELETE FROM ${spec.table} WHERE ${spec.repoColumn} = ?`).run(repoFullName); - return Number(info.changes); -} -/** - * Count rows for one repo in a store without deleting anything (#5564) — the read-only counterpart to - * `purgeStoreByRepo`, used by `purge-cli.js --dry-run` to report what a real purge would remove. - */ -export function countStoreByRepo(db, spec, repoFullName) { - for (const identifier of [spec.table, spec.repoColumn]) { - if (!SQL_IDENTIFIER.test(identifier)) - throw new Error(`unsafe SQL identifier: ${identifier}`); - } - const row = db.prepare(`SELECT COUNT(*) AS count FROM ${spec.table} WHERE ${spec.repoColumn} = ?`).get(repoFullName); - return Number(row?.count); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmUtbWFpbnRlbmFuY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzdG9yZS1tYWludGVuYW5jZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSx5R0FBeUc7QUFDekcsRUFBRTtBQUNGLGtHQUFrRztBQUNsRywrR0FBK0c7QUFDL0csb0ZBQW9GO0FBQ3BGLDJHQUEyRztBQUMzRyxpSEFBaUg7QUFDakgsaUZBQWlGO0FBQ2pGLDhHQUE4RztBQUM5RywwR0FBMEc7QUFDMUcsK0ZBQStGO0FBQy9GLGlIQUFpSDtBQUNqSCx3RkFBd0Y7QUFDeEYsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyQyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzNDLE9BQU8sRUFBRSxnQ0FBZ0MsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRTdFLHFFQUFxRTtBQUNyRSxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyxzQ0FBc0MsQ0FBQztBQUNoRixNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRywwQ0FBMEMsQ0FBQztBQUl4RjswR0FDMEc7QUFDMUcsTUFBTSxDQUFDLE1BQU0sMkJBQTJCLEdBQXdCLEVBQUUsS0FBSyxFQUFFLG9CQUFvQixFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxDQUFDO0FBQ2xKLE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUF3QixFQUFFLEtBQUssRUFBRSxpQkFBaUIsRUFBRSxlQUFlLEVBQUUsSUFBSSxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUMxSSxNQUFNLENBQUMsTUFBTSxnQ0FBZ0MsR0FBd0IsRUFBRSxLQUFLLEVBQUUsYUFBYSxFQUFFLGVBQWUsRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxDQUFDO0FBSXhJOzs7K0dBRytHO0FBQy9HLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFvQixFQUFFLEtBQUss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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/submission-freshness-check.d.ts b/packages/loopover-miner/lib/submission-freshness-check.d.ts deleted file mode 100644 index 05a25325bf..0000000000 --- a/packages/loopover-miner/lib/submission-freshness-check.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -export declare const SUBMISSION_FRESHNESS_ABORT_EVENT: "submission_freshness_abort"; -export type FreshnessAbortReason = "issue_closed" | "already_addressed" | "claim_superseded" | "live_state_unavailable"; -export type SubmissionFreshnessCandidate = { - repoFullName: string; - issueNumber: number; - minerLogin: string; -}; -export type LiveIssueSnapshot = { - state: "open" | "closed"; - referencingPrs: Array<{ - number: number; - state: "open" | "closed" | "merged"; - authorLogin: string; - createdAt: string | null; - }>; -}; -export type SubmissionFreshnessClaimLedger = { - listClaims(filter: { - repoFullName?: string; - status?: string; - }): Array<{ - repoFullName: string; - issueNumber: number; - status: string; - }>; -}; -export type SubmissionFreshnessEventLedger = { - appendEvent(event: { - type: string; - repoFullName?: string; - payload: Record; - }): unknown; -}; -export type SubmissionFreshnessDeps = { - claimLedger: SubmissionFreshnessClaimLedger; - fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise; - eventLedger: SubmissionFreshnessEventLedger; -}; -export type SubmissionFreshnessResult = { - fresh: true; -} | { - fresh: false; - reason: FreshnessAbortReason; -}; -export type SubmissionFreshnessRetryOptions = { - maxAttempts?: number; - sleepFn?: (ms: number) => Promise; - backoffMs?: (attempt: number) => number; -}; -/** - * Evaluate whether a submission candidate's live repo state is still fresh enough to proceed toward open_pr. - * Checks the miner's own claim-ledger status first (local, free) before spending a network round-trip on the - * live issue/PR snapshot. Fails closed (throws) on a malformed candidate or missing dependency. - * - * Bounded retry for the live-state snapshot fetch (#7089): up to `maxAttempts` (default 3) attempts with - * `backoffMs(attempt)` backoff between them, returning as soon as a real (non-null, well-formed) snapshot is - * obtained. Optional -- every existing caller works unchanged. Pure over the injected `sleepFn`/`backoffMs` - * -- no real timers in tests. Only the fetch itself is retried; a well-formed snapshot's own signals - * (issue_closed / already_addressed) are decided once, never retried. - */ -export declare function checkSubmissionFreshness(candidate: SubmissionFreshnessCandidate, deps: SubmissionFreshnessDeps, options?: SubmissionFreshnessRetryOptions): Promise; diff --git a/packages/loopover-miner/lib/submission-freshness-check.js b/packages/loopover-miner/lib/submission-freshness-check.js deleted file mode 100644 index b80bd76c13..0000000000 --- a/packages/loopover-miner/lib/submission-freshness-check.js +++ /dev/null @@ -1,120 +0,0 @@ -// Late-binding freshness check before open_pr fires (#3007). A soft-claim made at the start of a long -// create/iterate loop can go stale by the time a candidate reaches submission: the target issue may have been -// closed, already fixed by another author, or the miner's own claim may have been released/expired in the -// interim. This is a FINAL, read-only check immediately before open_pr spec construction -- complementing, not -// replacing, the claim-time check (src/miner/soft-claim.ts) -- so a stale submission never reaches the Governor -// chokepoint (governor-chokepoint.js) as a live write attempt: check freshness first, THEN prepareOpenPrSubmission -// (harness-submission-trigger.js), THEN the Governor. These are separate, sequentially-composed units, not nested -// calls -- attempt-runner.js (#2337) is the real call site that wires them together in that order. -// -// READ-ONLY BY CONTRACT: never writes anything except its own abort-reason audit event (on staleness only, not -// on every check -- mirrors this issue's own "log the abort reason" wording, not a per-decision audit trail). -// The live-state fetch is an injected dependency so this stays testable without real network I/O and agnostic -// to HOW the caller sources issue/PR state (raw GitHub API, loopover's own cached MCP data, etc.). -// -// FAIL CLOSED: an unreachable/failed live-state fetch is treated as stale (aborts), never as "no evidence of -// staleness, so proceed" -- mirrors this package's fail-closed convention elsewhere (harness-submission- -// trigger.js's predicted_gate_unavailable/slop_assessment_unavailable, iterate-loop.ts's ambiguous-on-error). -// That fail-closed OUTCOME is unchanged; a single transient blip just gets a bounded retry-with-backoff to -// resolve itself FIRST (#7089) -- the same window claim-conflict-resolver.js's resolveClaimConflict (#6058) -// already gives its own call to this identical fetchLiveIssueSnapshot, reusing http-retry.js's shared backoff. -// Aborting here discards a fully-completed create/iterate loop's local work, so riding out a brief 5xx / -// GraphQL-index propagation lag before failing closed matters more here than in the post-submission case. -// -// NOT a rejection outcome: staleness is caught BEFORE any PR exists, so it is not the same lifecycle event as -// rejection-state-machine.js's DISENGAGED_OUTCOME (which handles an EXISTING PR a maintainer closed). "No PR, -// no noisy failure" here just means: return a quiet not-fresh result, same shape as any other blocked gate -// decision in this package -- never throw, never surface anything to the target repo. -import { defaultRetryBackoffMs } from "./http-retry.js"; -export const SUBMISSION_FRESHNESS_ABORT_EVENT = "submission_freshness_abort"; -// Bounded retry for the pre-submission live-state fetch (#7089), mirroring claim-conflict-resolver.js's -// resolveClaimConflict (#6058): a few attempts with exponential backoff let a transient GitHub blur (a brief -// 5xx, or GraphQL-index propagation lag) resolve itself before we fail closed, without an unbounded loop. -const DEFAULT_SNAPSHOT_MAX_ATTEMPTS = 3; -const defaultSnapshotSleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); -/** - * Evaluate whether a submission candidate's live repo state is still fresh enough to proceed toward open_pr. - * Checks the miner's own claim-ledger status first (local, free) before spending a network round-trip on the - * live issue/PR snapshot. Fails closed (throws) on a malformed candidate or missing dependency. - * - * Bounded retry for the live-state snapshot fetch (#7089): up to `maxAttempts` (default 3) attempts with - * `backoffMs(attempt)` backoff between them, returning as soon as a real (non-null, well-formed) snapshot is - * obtained. Optional -- every existing caller works unchanged. Pure over the injected `sleepFn`/`backoffMs` - * -- no real timers in tests. Only the fetch itself is retried; a well-formed snapshot's own signals - * (issue_closed / already_addressed) are decided once, never retried. - */ -export async function checkSubmissionFreshness(candidate, deps, options = {}) { - if (!candidate || typeof candidate !== "object") - throw new Error("invalid_freshness_candidate"); - const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : ""; - if (!repoFullName) - throw new Error("invalid_repo_full_name"); - if (!Number.isInteger(candidate.issueNumber) || candidate.issueNumber < 1) - throw new Error("invalid_issue_number"); - const minerLogin = typeof candidate.minerLogin === "string" ? candidate.minerLogin.trim() : ""; - if (!minerLogin) - throw new Error("invalid_miner_login"); - if (!deps || typeof deps !== "object") - throw new Error("invalid_freshness_deps"); - const { claimLedger, fetchLiveIssueSnapshot, eventLedger } = deps; - if (!claimLedger || typeof claimLedger.listClaims !== "function") - throw new Error("invalid_claim_ledger"); - if (typeof fetchLiveIssueSnapshot !== "function") - throw new Error("invalid_live_state_fetcher"); - if (!eventLedger || typeof eventLedger.appendEvent !== "function") - throw new Error("invalid_event_ledger"); - const maxAttempts = Number.isFinite(options.maxAttempts) && options.maxAttempts >= 1 - ? Math.floor(options.maxAttempts) - : DEFAULT_SNAPSHOT_MAX_ATTEMPTS; - const sleepFn = typeof options.sleepFn === "function" ? options.sleepFn : defaultSnapshotSleep; - const backoffMs = typeof options.backoffMs === "function" ? options.backoffMs : defaultRetryBackoffMs; - const claim = claimLedger.listClaims({ repoFullName }).find((c) => c.issueNumber === candidate.issueNumber); - if (!claim || claim.status !== "active") { - return abort(eventLedger, repoFullName, candidate.issueNumber, "claim_superseded"); - } - let snapshot = null; - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - let current; - try { - current = await fetchLiveIssueSnapshot(repoFullName, candidate.issueNumber); - } - catch { - current = null; - } - if (current && typeof current === "object") { - // A real, well-formed snapshot resolves the transient window: stop retrying and decide on it now. - snapshot = current; - break; - } - // Back off before the next attempt (transient 5xx / index-propagation lag); never after the last one. - if (attempt < maxAttempts) - await sleepFn(backoffMs(attempt)); - } - if (!snapshot) { - // Retry budget exhausted with no usable snapshot -- fail closed exactly as before (#7089 only widens the window). - return abort(eventLedger, repoFullName, candidate.issueNumber, "live_state_unavailable"); - } - if (snapshot.state === "closed") { - return abort(eventLedger, repoFullName, candidate.issueNumber, "issue_closed"); - } - // GitHub logins are case-insensitive for identity purposes (the same account can be echoed back with - // different casing by different API responses), so a strict `!==` would misclassify the miner's own - // referencing PR as "another author" whenever the casing happens to differ -- compare case-normalized. - const minerLoginKey = minerLogin.toLowerCase(); - const referencingPrs = Array.isArray(snapshot.referencingPrs) ? snapshot.referencingPrs : []; - const addressedByAnotherAuthor = referencingPrs.some((pr) => (pr.state === "merged" || pr.state === "open") && - (typeof pr.authorLogin !== "string" || pr.authorLogin.trim().toLowerCase() !== minerLoginKey)); - if (addressedByAnotherAuthor) { - return abort(eventLedger, repoFullName, candidate.issueNumber, "already_addressed"); - } - return { fresh: true }; -} -function abort(eventLedger, repoFullName, issueNumber, reason) { - eventLedger.appendEvent({ - type: SUBMISSION_FRESHNESS_ABORT_EVENT, - repoFullName, - payload: { issueNumber, reason }, - }); - return { fresh: false, reason }; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3VibWlzc2lvbi1mcmVzaG5lc3MtY2hlY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzdWJtaXNzaW9uLWZyZXNobmVzcy1jaGVjay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxzR0FBc0c7QUFDdEcsOEdBQThHO0FBQzlHLDBHQUEwRztBQUMxRywrR0FBK0c7QUFDL0csZ0hBQWdIO0FBQ2hILG1IQUFtSDtBQUNuSCxrSEFBa0g7QUFDbEgsbUdBQW1HO0FBQ25HLEVBQUU7QUFDRiwrR0FBK0c7QUFDL0csOEdBQThHO0FBQzlHLDhHQUE4RztBQUM5RyxtR0FBbUc7QUFDbkcsRUFBRTtBQUNGLDZHQUE2RztBQUM3Ryx5R0FBeUc7QUFDekcsOEdBQThHO0FBQzlHLDJHQUEyRztBQUMzRyw0R0FBNEc7QUFDNUcsK0dBQStHO0FBQy9HLHlHQUF5RztBQUN6RywwR0FBMEc7QUFDMUcsRUFBRTtBQUNGLDhHQUE4RztBQUM5Ryw4R0FBOEc7QUFDOUcsMkdBQTJHO0FBQzNHLHNGQUFzRjtBQUV0RixPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUV4RCxNQUFNLENBQUMsTUFBTSxnQ0FBZ0MsR0FBRyw0QkFBcUMsQ0FBQztBQXFDdEYsd0dBQXdHO0FBQ3hHLDZHQUE2RztBQUM3RywwR0FBMEc7QUFDMUcsTUFBTSw2QkFBNkIsR0FBRyxDQUFDLENBQUM7QUFDeEMsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLE9BQWUsRUFBaUIsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUM7QUFFeEg7Ozs7Ozs7Ozs7R0FVRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsd0JBQXdCLENBQzVDLFNBQXVDLEVBQ3ZDLElBQTZCLEVBQzdCLFVBQTJDLEVBQUU7SUFFN0MsSUFBSSxDQUFDLFNBQVMsSUFBSSxPQUFPLFNBQVMsS0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/tenant-cli.d.ts b/packages/loopover-miner/lib/tenant-cli.d.ts deleted file mode 100644 index 158337cc22..0000000000 --- a/packages/loopover-miner/lib/tenant-cli.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createTenant, destroyTenant, listTenants } from "./tenant-client.js"; -export type ParsedTenantCreateArgs = { - name: string; - json: boolean; - product?: string; -} | { - error: string; -}; -export type ParsedTenantNameArgs = { - name: string; - json: boolean; -} | { - error: string; -}; -export type ParsedTenantListArgs = { - json: boolean; -} | { - error: string; -}; -export type RunTenantOptions = { - /** Read for the control-plane opt-in gate -- defaults to `process.env` inside the client. */ - env?: Record; - /** Injected fetch, forwarded to the client; defaults to the real global fetch. */ - fetchImpl?: (url: string, init: RequestInit) => Promise; - /** Injectable client functions so tests drive the CLI without a real control plane. */ - createTenant?: typeof createTenant; - listTenants?: typeof listTenants; - destroyTenant?: typeof destroyTenant; -}; -/** Parse `create [--product

] [--json]`. Returns `{ name, product, json }` or `{ error }`. */ -export declare function parseTenantCreateArgs(args: string[]): ParsedTenantCreateArgs; -/** Parse ` [--json]` for the single-positional commands (destroy). Returns `{ name, json }` or `{ error }`. */ -export declare function parseTenantNameArgs(args: string[]): ParsedTenantNameArgs; -/** Parse `list [--json]` (no positional). Returns `{ json }` or `{ error }`. */ -export declare function parseTenantListArgs(args: string[]): ParsedTenantListArgs; -export declare function runTenantCreate(args: string[], options?: RunTenantOptions): Promise; -export declare function runTenantList(args: string[], options?: RunTenantOptions): Promise; -export declare function runTenantDestroy(args: string[], options?: RunTenantOptions): Promise; -export declare function runTenantCli(subcommand: string | undefined, args: string[], options?: RunTenantOptions): Promise; diff --git a/packages/loopover-miner/lib/tenant-cli.js b/packages/loopover-miner/lib/tenant-cli.js deleted file mode 100644 index 489438cf38..0000000000 --- a/packages/loopover-miner/lib/tenant-cli.js +++ /dev/null @@ -1,150 +0,0 @@ -/** `tenant` CLI command group (#7275): create / list / destroy hosted tenant instances against the #7173 ORB+AMS - * hosting control-plane's provisioning API (#7180). Thin composition layer -- argv parsing plus a call into - * tenant-client.js, which owns the env-gated, Bearer-authed, FAIL-LOUD HTTP surface. Every failure the client - * throws (disabled/unconfigured plane, unreachable host, non-2xx, malformed body) is reported here as a non-zero - * exit with the client's own message; there is deliberately no silent-degrade path, because provisioning a tenant - * is a deliberate admin action whose failure an operator must see. Lifecycle `state` values are printed verbatim - * from the API -- this layer invents no state vocabulary of its own. */ -import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; -import { createTenant, destroyTenant, listTenants } from "./tenant-client.js"; -const TENANT_USAGE = "Usage: loopover-miner tenant [] [--product ] [--json]"; -/** Parse `create [--product

] [--json]`. Returns `{ name, product, json }` or `{ error }`. */ -export function parseTenantCreateArgs(args) { - let name = null; - let product = null; - let json = false; - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - json = true; - continue; - } - if (token === "--product") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) - return { error: TENANT_USAGE }; - product = value; - index += 1; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - if (name !== null) - return { error: TENANT_USAGE }; - name = token; - } - if (name === null) - return { error: TENANT_USAGE }; - return { name, json, ...(product !== null ? { product } : {}) }; -} -/** Parse ` [--json]` for the single-positional commands (destroy). Returns `{ name, json }` or `{ error }`. */ -export function parseTenantNameArgs(args) { - let name = null; - let json = false; - for (const token of args) { - if (token === "--json") { - json = true; - continue; - } - if (token.startsWith("-")) - return { error: `Unknown option: ${token}` }; - if (name !== null) - return { error: TENANT_USAGE }; - name = token; - } - if (name === null) - return { error: TENANT_USAGE }; - return { name, json }; -} -/** Parse `list [--json]` (no positional). Returns `{ json }` or `{ error }`. */ -export function parseTenantListArgs(args) { - let json = false; - for (const token of args) { - if (token === "--json") { - json = true; - continue; - } - return { error: `Unknown option: ${token}` }; - } - return { json }; -} -function renderTenantRecord(record) { - const name = typeof record.name === "string" ? record.name : "(unknown)"; - const product = typeof record.product === "string" ? record.product : "(unknown)"; - const state = typeof record.state === "string" ? record.state : "(unknown)"; - return `${name} product=${product} state=${state}`; -} -export async function runTenantCreate(args, options = {}) { - const parsed = parseTenantCreateArgs(args); - if ("error" in parsed) - return reportCliFailure(argsWantJson(args), parsed.error); - const create = options.createTenant ?? createTenant; - try { - const record = await create(parsed.name, { - env: options.env, - fetchImpl: options.fetchImpl, - ...(parsed.product !== undefined ? { product: parsed.product } : {}), - }); - if (parsed.json) { - console.log(JSON.stringify(record, null, 2)); - } - else { - console.log(`created ${renderTenantRecord(record)}`); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runTenantList(args, options = {}) { - const parsed = parseTenantListArgs(args); - if ("error" in parsed) - return reportCliFailure(argsWantJson(args), parsed.error); - const list = options.listTenants ?? listTenants; - try { - const records = await list({ env: options.env, fetchImpl: options.fetchImpl }); - if (parsed.json) { - console.log(JSON.stringify(records, null, 2)); - } - else if (records.length === 0) { - console.log("no tenants"); - } - else { - console.log(records.map(renderTenantRecord).join("\n")); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runTenantDestroy(args, options = {}) { - const parsed = parseTenantNameArgs(args); - if ("error" in parsed) - return reportCliFailure(argsWantJson(args), parsed.error); - const destroy = options.destroyTenant ?? destroyTenant; - try { - const record = await destroy(parsed.name, { env: options.env, fetchImpl: options.fetchImpl }); - if (parsed.json) { - console.log(JSON.stringify(record, null, 2)); - } - else { - console.log(`destroyed ${renderTenantRecord(record)}`); - } - return 0; - } - catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } -} -export async function runTenantCli(subcommand, args, options = {}) { - if (subcommand === "create") - return runTenantCreate(args, options); - if (subcommand === "list") - return runTenantList(args, options); - if (subcommand === "destroy") - return runTenantDestroy(args, options); - return reportCliFailure(argsWantJson(args), TENANT_USAGE); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVuYW50LWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlbmFudC1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozt3RUFNd0U7QUFDeEUsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsYUFBYSxFQUFFLFdBQVcsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRzlFLE1BQU0sWUFBWSxHQUFHLDRGQUE0RixDQUFDO0FBbUJsSCx3R0FBd0c7QUFDeEcsTUFBTSxVQUFVLHFCQUFxQixDQUFDLElBQWM7SUFDbEQsSUFBSSxJQUFJLEdBQWtCLElBQUksQ0FBQztJQUMvQixJQUFJLE9BQU8sR0FBa0IsSUFBSSxDQUFDO0lBQ2xDLElBQUksSUFBSSxHQUFHLEtBQUssQ0FBQztJQUNqQixLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDcEQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBRSxDQUFDO1FBQzNCLElBQUksS0FBSyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3ZCLElBQUksR0FBRyxJQUFJLENBQUM7WUFDWixTQUFTO1FBQ1gsQ0FBQztRQUNELElBQUksS0FBSyxLQUFLLFdBQVcsRUFBRSxDQUFDO1lBQzFCLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFDOUIsSUFBSSxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztnQkFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLFlBQVksRUFBRSxDQUFDO1lBQ3BFLE9BQU8sR0FBRyxLQUFLLENBQUM7WUFDaEIsS0FBSyxJQUFJLENBQUMsQ0FBQztZQUNYLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztZQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsbUJBQW1CLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDeEUsSUFBSSxJQUFJLEtBQUssSUFBSTtZQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsWUFBWSxFQUFFLENBQUM7UUFDbEQsSUFBSSxHQUFHLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUFJLElBQUksS0FBSyxJQUFJO1FBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxZQUFZLEVBQUUsQ0FBQztJQUNsRCxPQUFPLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxHQUFHLENBQUMsT0FBTyxLQUFLLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQztBQUNsRSxDQUFDO0FBRUQscUhBQXFIO0FBQ3JILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxJQUFjO0lBQ2hELElBQUksSUFBSSxHQUFrQixJQUFJLENBQUM7SUFDL0IsSUFBSSxJQUFJLEdBQUcsS0FBSyxDQUFDO0lBQ2pCLEtBQUssTUFBTSxLQUFLLElBQUksSUFBSSxFQUFFLENBQUM7UUFDekIsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsSUFBSSxHQUFHLElBQUksQ0FBQztZQUNaLFNBQVM7UUFDWCxDQUFDO1FBQ0QsSUFBSSxLQUFLLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztZQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsbUJBQW1CLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDeEUsSUFBSSxJQUFJLEtBQUssSUFBSTtZQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsWUFBWSxFQUFFLENBQUM7UUFDbEQsSUFBSSxHQUFHLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUFJLElBQUksS0FBSyxJQUFJO1FBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxZQUFZLEVBQUUsQ0FBQztJQUNsRCxPQUFPLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxDQUFDO0FBQ3hCLENBQUM7QUFFRCxnRkFBZ0Y7QUFDaEYsTUFBTSxVQUFVLG1CQUFtQixDQUFDLElBQWM7SUFDaEQsSUFBSSxJQUFJLEdBQUcsS0FBSyxDQUFDO0lBQ2pCLEtBQUssTUFBTSxLQUFLLElBQUksSUFBSSxFQUFFLENBQUM7UUFDekIsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsSUFBSSxHQUFHLElBQUksQ0FBQztZQUNaLFNBQVM7UUFDWCxDQUFDO1FBQ0QsT0FBTyxFQUFFLEtBQUssRUFBRSxtQkFBbUIsS0FBSyxFQUFFLEVBQUUsQ0FBQztJQUMvQyxDQUFDO0lBQ0QsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDO0FBQ2xCLENBQUM7QUFFRCxTQUFTLGtCQUFrQixDQUFDLE1BQW9CO0lBQzlDLE1BQU0sSUFBSSxHQUFHLE9BQU8sTUFBTSxDQUFDLElBQUksS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQztJQUN6RSxNQUFNLE9BQU8sR0FBRyxPQUFPLE1BQU0sQ0FBQyxPQUFPLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUM7SUFDbEYsTUFBTSxLQUFLLEdBQUcsT0FBTyxNQUFNLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsV0FBVyxDQUFDO0lBQzVFLE9BQU8sR0FBRyxJQUFJLGFBQWEsT0FBTyxXQUFXLEtBQUssRUFBRSxDQUFDO0FBQ3ZELENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLGVBQWUsQ0FBQyxJQUFjLEVBQUUsVUFBNEIsRUFBRTtJQUNsRixNQUFNLE1BQU0sR0FBRyxxQkFBcUIsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMzQyxJQUFJLE9BQU8sSUFBSSxNQUFNO1FBQUUsT0FBTyxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2pGLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxZQUFZLElBQUksWUFBWSxDQUFDO0lBQ3BELElBQUksQ0FBQztRQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUU7WUFDdkMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxHQUFHO1lBQ2hCLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUztZQUM1QixHQUFHLENBQUMsTUFBTSxDQUFDLE9BQU8sS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1NBQzlDLENBQUMsQ0FBQztRQUMxQixJQUFJLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNoQixPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQy9DLENBQUM7YUFBTSxDQUFDO1lBQ04sT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUN2RCxDQUFDO1FBQ0QsT0FBTyxDQUFDLENBQUM7SUFDWCxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE9BQU8sZ0JBQWdCLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0lBQ2hFLENBQUM7QUFDSCxDQUFDO0FBRUQsTUFBTSxDQUFDLEtBQUssVUFBVSxhQUFhLENBQUMsSUFBYyxFQUFFLFVBQTRCLEVBQUU7SUFDaEYsTUFBTSxNQUFNLEdBQUcsbUJBQW1CLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDekMsSUFBSSxPQUFPLElBQUksTUFBTTtRQUFFLE9BQU8sZ0JBQWdCLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNqRixNQUFNLElBQUksR0FBRyxPQUFPLENBQUMsV0FBVyxJQUFJLFdBQVcsQ0FBQztJQUNoRCxJQUFJLENBQUM7UUFDSCxNQUFNLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxFQUFFLEdBQUcsRUFBRSxPQUFPLENBQUMsR0FBRyxFQUFFLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUyxFQUF5QixDQUFDLENBQUM7UUFDdEcsSUFBSSxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDaEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNoRCxDQUFDO2FBQU0sSUFBSSxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1lBQ2hDLE9BQU8sQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUM7UUFDNUIsQ0FBQzthQUFNLENBQUM7WUFDTixPQUFPLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUMxRCxDQUFDO1FBQ0QsT0FBTyxDQUFDLENBQUM7SUFDWCxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE9BQU8sZ0JBQWdCLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDO0lBQ2hFLENBQUM7QUFDSCxDQUFDO0FBRUQsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxJQUFjLEVBQUUsVUFBNEIsRUFBRTtJQUNuRixNQUFNLE1BQU0sR0FBRyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUN6QyxJQUFJLE9BQU8sSUFBSSxNQUFNO1FBQUUsT0FBTyxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2pGLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLElBQUksYUFBYSxDQUFDO0lBQ3ZELElBQUksQ0FBQztRQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsRUFBRSxHQUFHLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxTQUFTLEVBQUUsT0FBTyxDQUFDLFNBQVMsRUFBeUIsQ0FBQyxDQUFDO1FBQ3JILElBQUksTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDO1lBQ2hCLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDL0MsQ0FBQzthQUFNLENBQUM7WUFDTixPQUFPLENBQUMsR0FBRyxDQUFDLGFBQWEsa0JBQWtCLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQ3pELENBQUM7UUFDRCxPQUFPLENBQUMsQ0FBQztJQUNYLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsT0FBTyxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLGdCQUFnQixDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7SUFDaEUsQ0FBQztBQUNILENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLFlBQVksQ0FBQyxVQUE4QixFQUFFLElBQWMsRUFBRSxVQUE0QixFQUFFO0lBQy9HLElBQUksVUFBVSxLQUFLLFFBQVE7UUFBRSxPQUFPLGVBQWUsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDbkUsSUFBSSxVQUFVLEtBQUssTUFBTTtRQUFFLE9BQU8sYUFBYSxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQztJQUMvRCxJQUFJLFVBQVUsS0FBSyxTQUFTO1FBQUUsT0FBTyxnQkFBZ0IsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDckUsT0FBTyxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEVBQUUsWUFBWSxDQUFDLENBQUM7QUFDNUQsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/loopover-miner/lib/tenant-client.d.ts b/packages/loopover-miner/lib/tenant-client.d.ts deleted file mode 100644 index ca22593cd7..0000000000 --- a/packages/loopover-miner/lib/tenant-client.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** Admin client for the hosted control-plane's tenant-provisioning API (#7275, part of the #7173 ORB+AMS hosting - * control-plane; talks to #7180's provisioning API). Opt-in and completely inert unless - * LOOPOVER_MINER_CONTROL_PLANE is set AND a URL is configured -- mirroring discovery-index-client.js's env-gated, - * never-auto-enabled posture. But UNLIKE that client's deliberately fail-OPEN opportunistic supplement, every - * call here FAILS LOUD: create/list/destroy are deliberate admin actions, so a disabled/unconfigured/unreachable/ - * non-2xx/malformed-response condition throws a clear Error (surfaced by tenant-cli.js as a non-zero exit and - * message) rather than silently degrading. Bearer-authed with an ADMIN credential, distinct from any tenant's own - * per-instance secrets. Lifecycle states (`provisioning`/`active`/`suspended`/`torn down`) are passed through - * exactly as the API reports them -- no AMS-specific state vocabulary is invented here. A single bounded request - * per call (no retry): a create is not idempotent, so it must not be silently re-sent. */ -export declare const CONTROL_PLANE_FLAG = "LOOPOVER_MINER_CONTROL_PLANE"; -export declare const CONTROL_PLANE_URL_FLAG = "LOOPOVER_MINER_CONTROL_PLANE_URL"; -export declare const CONTROL_PLANE_ADMIN_TOKEN_FLAG = "LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN"; -export type TenantClientOptions = { - env?: Record; - /** Always called as `fetchImpl(url, init)` with a plain string URL -- narrower than `typeof fetch` on - * purpose, since that's the only shape this module ever actually calls it with. */ - fetchImpl?: (url: string, init: RequestInit) => Promise; - requestTimeoutMs?: number; -}; -export type CreateTenantOptions = TenantClientOptions & { - product?: string; -}; -/** A tenant record as reported by the control plane. Lifecycle `state` is passed through verbatim (the API owns - * the vocabulary, e.g. `provisioning` / `active` / `suspended` / `torn down`); other fields vary by product. */ -export type TenantRecord = Record; -/** Master opt-in (default off): no control-plane traffic is possible until this is truthy. */ -export declare function isControlPlaneEnabled(env?: Record): boolean; -/** - * Create a hosted tenant instance. Returns the created tenant record exactly as the control plane reports it - * (including its lifecycle `state`). `options.product` defaults to `"ams"`. - * - * @param {string} name - * @param {{ product?: string, env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - */ -export declare function createTenant(name: string, options?: CreateTenantOptions): Promise; -/** - * List all hosted tenant instances the admin credential can see. Returns the `tenants` array as reported. - * - * @param {{ env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - * @returns {Promise>>} - */ -export declare function listTenants(options?: TenantClientOptions): Promise; -/** - * Tear down a hosted tenant instance by name. Returns the control plane's final record for it (typically the - * transitional `torn down` lifecycle state). - * - * @param {string} name - * @param {{ env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - */ -export declare function destroyTenant(name: string, options?: TenantClientOptions): Promise; diff --git a/packages/loopover-miner/lib/tenant-client.js b/packages/loopover-miner/lib/tenant-client.js deleted file mode 100644 index 1b7cb3d3ee..0000000000 --- a/packages/loopover-miner/lib/tenant-client.js +++ /dev/null @@ -1,114 +0,0 @@ -/** Admin client for the hosted control-plane's tenant-provisioning API (#7275, part of the #7173 ORB+AMS hosting - * control-plane; talks to #7180's provisioning API). Opt-in and completely inert unless - * LOOPOVER_MINER_CONTROL_PLANE is set AND a URL is configured -- mirroring discovery-index-client.js's env-gated, - * never-auto-enabled posture. But UNLIKE that client's deliberately fail-OPEN opportunistic supplement, every - * call here FAILS LOUD: create/list/destroy are deliberate admin actions, so a disabled/unconfigured/unreachable/ - * non-2xx/malformed-response condition throws a clear Error (surfaced by tenant-cli.js as a non-zero exit and - * message) rather than silently degrading. Bearer-authed with an ADMIN credential, distinct from any tenant's own - * per-instance secrets. Lifecycle states (`provisioning`/`active`/`suspended`/`torn down`) are passed through - * exactly as the API reports them -- no AMS-specific state vocabulary is invented here. A single bounded request - * per call (no retry): a create is not idempotent, so it must not be silently re-sent. */ -export const CONTROL_PLANE_FLAG = "LOOPOVER_MINER_CONTROL_PLANE"; -export const CONTROL_PLANE_URL_FLAG = "LOOPOVER_MINER_CONTROL_PLANE_URL"; -export const CONTROL_PLANE_ADMIN_TOKEN_FLAG = "LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN"; -const TRUTHY_ENV_VALUE = /^(1|true|yes|on)$/i; -const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; -function isTruthyEnvValue(value) { - return TRUTHY_ENV_VALUE.test(String(value).trim()); -} -// Reads below use literal `env.LOOPOVER_MINER_*` property access (not the *_FLAG constants) because -// scripts/generate-env-reference.mjs statically greps for exactly this `env.NAME ?? "..."` shape to keep the -// generated env reference honest -- a dynamic `env[SOME_CONST]` lookup is invisible to it. -/** Master opt-in (default off): no control-plane traffic is possible until this is truthy. */ -export function isControlPlaneEnabled(env = process.env) { - return isTruthyEnvValue(env.LOOPOVER_MINER_CONTROL_PLANE ?? ""); -} -function resolveControlPlaneUrl(env) { - const raw = (env.LOOPOVER_MINER_CONTROL_PLANE_URL ?? "").trim(); - return raw ? raw.replace(/\/+$/, "") : null; -} -function resolveAdminToken(env) { - const raw = typeof env.LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN === "string" ? env.LOOPOVER_MINER_CONTROL_PLANE_ADMIN_TOKEN.trim() : ""; - return raw || null; -} -/** Resolve + validate the control-plane connection, or throw a clear admin-facing error (fail loud). */ -function resolveControlPlane(env) { - if (!isControlPlaneEnabled(env)) { - throw new Error(`control plane disabled: set ${CONTROL_PLANE_FLAG}=1 to enable tenant admin commands`); - } - const baseUrl = resolveControlPlaneUrl(env); - if (!baseUrl) - throw new Error(`control plane URL unconfigured: set ${CONTROL_PLANE_URL_FLAG}`); - const token = resolveAdminToken(env); - if (!token) - throw new Error(`control plane admin token unconfigured: set ${CONTROL_PLANE_ADMIN_TOKEN_FLAG}`); - return { baseUrl, token }; -} -/** - * One bounded, Bearer-authed request against the control plane. Throws a clear Error on any failure: disabled/ - * unconfigured plane, unreachable host or timeout, non-2xx status, or a non-JSON/non-object body. - * - * @param {"GET"|"POST"|"DELETE"} method - * @param {string} path - * @param {unknown} body request body (JSON-encoded), or undefined for none - * @param {{ env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} options - * @returns {Promise>} - */ -async function controlPlaneRequest(method, path, body, options) { - const env = options.env ?? process.env; - const { baseUrl, token } = resolveControlPlane(env); - const fetchImpl = options.fetchImpl ?? fetch; - const timeoutMs = Number.isFinite(options.requestTimeoutMs) ? options.requestTimeoutMs : DEFAULT_REQUEST_TIMEOUT_MS; - let response; - try { - response = await fetchImpl(`${baseUrl}${path}`, { - method, - headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, - ...(body === undefined ? {} : { body: JSON.stringify(body) }), - signal: AbortSignal.timeout(timeoutMs), - }); - } - catch (error) { - throw new Error(`control plane unreachable for ${method} ${path}: ${error instanceof Error ? error.message : String(error)}`); - } - if (!response.ok) { - throw new Error(`control plane returned http_${response.status} for ${method} ${path}`); - } - const payload = (await response.json().catch(() => null)); - if (payload === null || typeof payload !== "object") { - throw new Error(`control plane returned a malformed response for ${method} ${path}`); - } - return payload; -} -/** - * Create a hosted tenant instance. Returns the created tenant record exactly as the control plane reports it - * (including its lifecycle `state`). `options.product` defaults to `"ams"`. - * - * @param {string} name - * @param {{ product?: string, env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - */ -export async function createTenant(name, options = {}) { - const product = typeof options.product === "string" && options.product.trim() ? options.product.trim() : "ams"; - return controlPlaneRequest("POST", "/v1/tenants", { name, product }, options); -} -/** - * List all hosted tenant instances the admin credential can see. Returns the `tenants` array as reported. - * - * @param {{ env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - * @returns {Promise>>} - */ -export async function listTenants(options = {}) { - const payload = await controlPlaneRequest("GET", "/v1/tenants", undefined, options); - return Array.isArray(payload.tenants) ? payload.tenants : []; -} -/** - * Tear down a hosted tenant instance by name. Returns the control plane's final record for it (typically the - * transitional `torn down` lifecycle state). - * - * @param {string} name - * @param {{ env?: Record, fetchImpl?: typeof fetch, requestTimeoutMs?: number }} [options] - */ -export async function destroyTenant(name, options = {}) { - return controlPlaneRequest("DELETE", `/v1/tenants/${encodeURIComponent(name)}`, undefined, options); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVuYW50LWNsaWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlbmFudC1jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7OzswRkFTMEY7QUFFMUYsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsOEJBQThCLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsa0NBQWtDLENBQUM7QUFDekUsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsMENBQTBDLENBQUM7QUFrQnpGLE1BQU0sZ0JBQWdCLEdBQUcsb0JBQW9CLENBQUM7QUFDOUMsTUFBTSwwQkFBMEIsR0FBRyxNQUFNLENBQUM7QUFFMUMsU0FBUyxnQkFBZ0IsQ0FBQyxLQUFhO0lBQ3JDLE9BQU8sZ0JBQWdCLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQ3JELENBQUM7QUFFRCxvR0FBb0c7QUFDcEcsNkdBQTZHO0FBQzdHLDJGQUEyRjtBQUUzRiw4RkFBOEY7QUFDOUYsTUFBTSxVQUFVLHFCQUFxQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQ3pGLE9BQU8sZ0JBQWdCLENBQUMsR0FBRyxDQUFDLDRCQUE0QixJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQ2xFLENBQUM7QUFFRCxTQUFTLHNCQUFzQixDQUFDLEdBQXVDO0lBQ3JFLE1BQU0sR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLGdDQUFnQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ2hFLE9BQU8sR0FBRyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0FBQzlDLENBQUM7QUFFRCxTQUFTLGlCQUFpQixDQUFDLEdBQXVDO0lBQ2hFLE1BQU0sR0FBRyxHQUFHLE9BQU8sR0FBRyxDQUFDLHdD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/update-check.d.ts b/packages/loopover-miner/lib/update-check.d.ts deleted file mode 100644 index 2c640e8a6e..0000000000 --- a/packages/loopover-miner/lib/update-check.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -export declare function resolveNpmRegistryUrl(env?: Record): string; -export declare function resolveUpgradeCommand(packageName?: string): string; -export declare function shouldSkipUpdateCheck(cliArgs: string[], env?: Record): boolean; -export declare function compareSemver(a: string, b: string): -1 | 0 | 1 | null; -export declare function fetchLatestPackageVersion(input: { - packageName: string; - npmRegistryUrl: string; - timeoutMs?: number; -}): Promise; -export declare function maybePrintUpdateNudge(input: { - packageName: string; - packageVersion: string; - npmRegistryUrl: string; - upgradeCommand: string; - timeoutMs?: number; -}): Promise; -export declare function startUpdateCheck(cliArgs: string[], input: { - packageName: string; - packageVersion: string; - upgradeCommand?: string; - env?: Record; - timeoutMs?: number; -}): Promise; -export declare const updateCheckExitGraceMs = 250; -export declare function awaitOpportunisticUpdateCheck(updateCheck: Promise, graceMs?: number): Promise; diff --git a/packages/loopover-miner/lib/update-check.js b/packages/loopover-miner/lib/update-check.js deleted file mode 100644 index f16e69c886..0000000000 --- a/packages/loopover-miner/lib/update-check.js +++ /dev/null @@ -1,162 +0,0 @@ -const defaultPackageName = "@loopover/miner"; -const defaultNpmRegistryUrl = "https://registry.npmjs.org"; -function isLocalRegistryHost(hostname) { - const normalized = hostname.toLowerCase().replace(/\.$/, ""); - return (normalized === "localhost" || - normalized === "127.0.0.1" || - normalized === "::1" || - normalized === "[::1]"); -} -export function resolveNpmRegistryUrl(env = process.env) { - const raw = env.LOOPOVER_NPM_REGISTRY_URL?.trim(); - if (!raw) - return defaultNpmRegistryUrl; - let url; - try { - url = new URL(raw); - } - catch { - return defaultNpmRegistryUrl; - } - if (url.username || url.password || url.search || url.hash || !url.hostname) { - return defaultNpmRegistryUrl; - } - const local = isLocalRegistryHost(url.hostname); - if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) { - return defaultNpmRegistryUrl; - } - const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, ""); - return `${url.origin}${path}`; -} -export function resolveUpgradeCommand(packageName = defaultPackageName) { - return `npm install -g ${packageName}@latest`; -} -export function shouldSkipUpdateCheck(cliArgs, env = process.env) { - if (/^(1|true|yes)$/i.test(env.LOOPOVER_MINER_NO_UPDATE_CHECK ?? "")) - return true; - return cliArgs.includes("--no-update-check"); -} -// `version` is always a real string here: this function is private and only ever called by -// compareSemver(a: string, b: string), whose own signature guarantees that -- so trimming it directly -// is safe and equivalent to the historical `String(version ?? "").trim()` for every reachable input. -function parseSemver(version) { - const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim()); - if (!match) - return null; - return { - major: Number(match[1]), - minor: Number(match[2]), - patch: Number(match[3]), - prerelease: match[4] ?? null, - }; -} -// Numeric identifiers are compared as decimal strings, not via Number(), which loses precision beyond -// Number.MAX_SAFE_INTEGER (2^53-1): two distinct digit strings past that width can round to the SAME float, -// making Number(leftId) !== Number(rightId) wrongly report them as equal (mirrors the same fix already applied -// to compareMcpSemver's comparePrerelease in src/services/mcp-compatibility.ts, #3049). With no leading zeros -// (semver's own numeric-identifier rule), a longer digit string is the larger number, and equal-length strings -// compare lexicographically. -function comparePrerelease(a, b) { - const left = a.split("."); - const right = b.split("."); - for (let index = 0; index < Math.max(left.length, right.length); index += 1) { - const leftId = left[index]; - const rightId = right[index]; - if (leftId === undefined) - return -1; - if (rightId === undefined) - return 1; - const leftNumeric = /^\d+$/.test(leftId); - const rightNumeric = /^\d+$/.test(rightId); - if (leftNumeric && rightNumeric) { - if (leftId.length !== rightId.length) - return leftId.length < rightId.length ? -1 : 1; - if (leftId !== rightId) - return leftId < rightId ? -1 : 1; - } - else if (leftNumeric !== rightNumeric) { - return leftNumeric ? -1 : 1; - } - else if (leftId !== rightId) { - return leftId < rightId ? -1 : 1; - } - } - // Unreachable via this function's only caller: compareSemver calls comparePrerelease only when the two raw - // prerelease strings already differ, and if every split segment above compared equal, the two strings would - // necessarily be identical too -- contradicting that guard. Kept for comparePrerelease's own general "equal" - // contract (the type signature promises 0 is a possible result) rather than asserting a case that can't occur. - return 0; -} -export function compareSemver(a, b) { - const left = parseSemver(a); - const right = parseSemver(b); - if (!left || !right) - return null; - for (const part of ["major", "minor", "patch"]) { - if (left[part] !== right[part]) - return left[part] < right[part] ? -1 : 1; - } - if (left.prerelease === right.prerelease) - return 0; - if (left.prerelease === null) - return 1; - if (right.prerelease === null) - return -1; - return comparePrerelease(left.prerelease, right.prerelease); -} -export async function fetchLatestPackageVersion(input) { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), input.timeoutMs ?? 5000); - const registrySlug = input.packageName.startsWith("@") - ? input.packageName.replace("/", "%2F") - : input.packageName; - const registryPath = `${input.npmRegistryUrl}/${registrySlug}/latest`; - try { - const response = await fetch(registryPath, { - signal: controller.signal, - headers: { accept: "application/json" }, - }); - const payload = await response.json().catch(() => ({})); - if (!response.ok || typeof payload.version !== "string") - throw new Error("npm_latest_version_unavailable"); - return payload.version; - } - finally { - clearTimeout(timeout); - } -} -// Non-blocking startup nudge: prints one upgrade line when local is behind npm latest. -// Mirrors packages/loopover-mcp/bin/loopover-mcp.js packageVersion/npmRegistryUrl/upgradeCommand (#2331). -export async function maybePrintUpdateNudge(input) { - try { - const latestVersion = await fetchLatestPackageVersion(input); - const comparison = compareSemver(input.packageVersion, latestVersion); - if (comparison !== null && comparison < 0) { - process.stderr.write(`${input.upgradeCommand}\n`); - } - } - catch { - // Offline or unreachable registry — never block or fail the CLI. - } -} -export function startUpdateCheck(cliArgs, input) { - if (shouldSkipUpdateCheck(cliArgs, input.env)) - return Promise.resolve(); - return maybePrintUpdateNudge({ - packageName: input.packageName, - packageVersion: input.packageVersion, - npmRegistryUrl: resolveNpmRegistryUrl(input.env), - upgradeCommand: input.upgradeCommand ?? resolveUpgradeCommand(input.packageName), - timeoutMs: input.timeoutMs, - }); -} -export const updateCheckExitGraceMs = 250; -// After command output is printed, give a fast registry response time to emit the nudge -// without waiting for the full lookup timeout on slow/offline registries. -export async function awaitOpportunisticUpdateCheck(updateCheck, graceMs = updateCheckExitGraceMs) { - await Promise.race([ - updateCheck.catch(() => undefined), - new Promise((resolve) => setTimeout(resolve, graceMs)), - ]); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBkYXRlLWNoZWNrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidXBkYXRlLWNoZWNrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE1BQU0sa0JBQWtCLEdBQUcsaUJBQWlCLENBQUM7QUFDN0MsTUFBTSxxQkFBcUIsR0FBRyw0QkFBNEIsQ0FBQztBQUUzRCxTQUFTLG1CQUFtQixDQUFDLFFBQWdCO0lBQzNDLE1BQU0sVUFBVSxHQUFHLFFBQVEsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQzdELE9BQU8sQ0FDTCxVQUFVLEtBQUssV0FBVztRQUMxQixVQUFVLEtBQUssV0FBVztRQUMxQixVQUFVLEtBQUssS0FBSztRQUNwQixVQUFVLEtBQUssT0FBTyxDQUN2QixDQUFDO0FBQ0osQ0FBQztBQUVELE1BQU0sVUFBVSxxQkFBcUIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUN6RixNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMseUJBQXlCLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDbEQsSUFBSSxDQUFDLEdBQUc7UUFBRSxPQUFPLHFCQUFxQixDQUFDO0lBRXZDLElBQUksR0FBUSxDQUFDO0lBQ2IsSUFBSSxDQUFDO1FBQ0gsR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ3JCLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLHFCQUFxQixDQUFDO0lBQy9CLENBQUM7SUFFRCxJQUFJLEdBQUcsQ0FBQyxRQUFRLElBQUksR0FBRyxDQUFDLFFBQVEsSUFBSSxHQUFHLENBQUMsTUFBTSxJQUFJLEdBQUcsQ0FBQyxJQUFJLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDNUUsT0FBTyxxQkFBcUIsQ0FBQztJQUMvQixDQUFDO0lBRUQsTUFBTSxLQUFLLEdBQUcsbUJBQW1CLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFD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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/version.d.ts b/packages/loopover-miner/lib/version.d.ts deleted file mode 100644 index 1be9f1defc..0000000000 --- a/packages/loopover-miner/lib/version.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** Package.json semver at import time — the laptop npm-install default. */ -export declare const MINER_PACKAGE_VERSION: string; -/** Resolved miner release id: `LOOPOVER_MINER_VERSION` wins when set (fleet Docker image builds). */ -export declare function resolveMinerVersion(env?: Record): string; diff --git a/packages/loopover-miner/lib/version.js b/packages/loopover-miner/lib/version.js deleted file mode 100644 index c3ff373653..0000000000 --- a/packages/loopover-miner/lib/version.js +++ /dev/null @@ -1,9 +0,0 @@ -import ownPackageJson from "../package.json" with { type: "json" }; -/** Package.json semver at import time — the laptop npm-install default. */ -export const MINER_PACKAGE_VERSION = ownPackageJson.version; -/** Resolved miner release id: `LOOPOVER_MINER_VERSION` wins when set (fleet Docker image builds). */ -export function resolveMinerVersion(env = process.env) { - const override = typeof env.LOOPOVER_MINER_VERSION === "string" ? env.LOOPOVER_MINER_VERSION.trim() : ""; - return override || MINER_PACKAGE_VERSION; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInZlcnNpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxjQUFjLE1BQU0saUJBQWlCLENBQUMsT0FBTyxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFFbkUsMkVBQTJFO0FBQzNFLE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFXLGNBQWMsQ0FBQyxPQUFPLENBQUM7QUFFcEUscUdBQXFHO0FBQ3JHLE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUN2RixNQUFNLFFBQVEsR0FBRyxPQUFPLEdBQUcsQ0FBQyxzQkFBc0IsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3pHLE9BQU8sUUFBUSxJQUFJLHFCQUFxQixDQUFDO0FBQzNDLENBQUMifQ== \ No newline at end of file diff --git a/packages/loopover-miner/lib/worktree-allocator.d.ts b/packages/loopover-miner/lib/worktree-allocator.d.ts deleted file mode 100644 index 02b4b23bdf..0000000000 --- a/packages/loopover-miner/lib/worktree-allocator.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type WorktreeAllocation = { - slotIndex: number; - worktreePath: string; - attemptId: string | null; - repoFullName: string | null; - status: "free" | "active"; - ownerPid: number | null; - ownerHost: string | null; - allocatedAt: string | null; -}; -export type WorktreeAllocator = { - dbPath: string; - worktreeBaseDir: string; - maxConcurrency: number; - maxLeaseMs: number; - processPid: number; - hostId: string; - acquire(attemptId: string, repoFullName: string): WorktreeAllocation; - release(attemptId: string): WorktreeAllocation | null; - listSlots(): WorktreeAllocation[]; - close(): void; -}; -export declare const DEFAULT_MAX_LEASE_MS: number; -export declare function resolveWorktreeAllocatorDbPath(env?: Record): string; -export declare function resolveWorktreeBaseDir(env?: Record): string; -export declare function isProcessAlive(pid: number): boolean; -/** - * Opens the local worktree allocator store. On startup reclaims orphaned active slots — any slot past its - * `maxLeaseMs` age (the container-agnostic guarantee for fleet mode's shared store), plus, as a same-host fast - * path, any slot whose owner pid is confirmed dead in THIS host's PID namespace. - */ -export declare function openWorktreeAllocator(options?: { - dbPath?: string; - worktreeBaseDir?: string; - maxConcurrency?: number; - maxLeaseMs?: number; - processPid?: number; - hostId?: string; - nowMs?: number; -}): WorktreeAllocator; -export declare function acquireWorktree(attemptId: string, repoFullName: string): WorktreeAllocation; -export declare function releaseWorktree(attemptId: string): WorktreeAllocation | null; -export declare function closeDefaultWorktreeAllocator(): void; diff --git a/packages/loopover-miner/lib/worktree-allocator.js b/packages/loopover-miner/lib/worktree-allocator.js deleted file mode 100644 index 15ae8c90ac..0000000000 --- a/packages/loopover-miner/lib/worktree-allocator.js +++ /dev/null @@ -1,312 +0,0 @@ -// mkdirSync is still needed for the git-worktree CHECKOUT dirs below (resolveWorktreeBaseDir's tree) — that is -// a filesystem directory, not a store DB path, and is deliberately out of this migration's scope. Only the DB -// handle's own mkdir/chmod moved into openLocalStoreDb. -import { mkdirSync } from "node:fs"; -import { homedir, hostname } from "node:os"; -import { join } from "node:path"; -import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js"; -import { isValidRepoSegment } from "./repo-clone.js"; -const defaultDbFileName = "worktree-allocator.sqlite3"; -const defaultWorktreeDirName = "worktrees"; -const defaultMaxConcurrency = 2; -let defaultWorktreeAllocator = null; -// Age-based orphan reclaim (#7085). Fleet mode (see DEPLOYMENT.md) runs multiple separate CONTAINERS over one -// shared data volume, each with its own PID namespace, so a stored `owner_pid` is meaningless the moment a -// different container opens this store — `isProcessAlive` checks the CALLING process's own namespace, not the -// one that recorded the pid. So we mirror the age-based convention every sibling shared-lease store already uses -// (portfolio-queue-expiry.js's DEFAULT_MAX_LEASE_MS / sweepStuckItems, claim-ledger's DEFAULT_MAX_CLAIM_AGE_MS): -// reclaim any `active` slot older than this regardless of what the pid check reports. Kept well above -// portfolio-queue-expiry's 30-minute floor because a single worktree lease spans a whole coding attempt (clone + -// agent run + push), which can legitimately run for hours; the same-host `isProcessAlive` fast path still frees a -// crashed local owner immediately, so this age fallback only ever governs the cross-container case. -export const DEFAULT_MAX_LEASE_MS = 6 * 60 * 60 * 1000; -export function resolveWorktreeAllocatorDbPath(env = process.env) { - return resolveLocalStoreDbPath(defaultDbFileName, "LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB", env); -} -export function resolveWorktreeBaseDir(env = process.env) { - const explicitPath = typeof env.LOOPOVER_MINER_WORKTREE_DIR === "string" - ? env.LOOPOVER_MINER_WORKTREE_DIR.trim() - : ""; - if (explicitPath) - return explicitPath; - const explicitConfigDir = typeof env.LOOPOVER_MINER_CONFIG_DIR === "string" - ? env.LOOPOVER_MINER_CONFIG_DIR.trim() - : ""; - if (explicitConfigDir) - return join(explicitConfigDir, defaultWorktreeDirName); - const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim() - ? env.XDG_CONFIG_HOME.trim() - : join(homedir(), ".config"); - return join(configHome, "loopover-miner", defaultWorktreeDirName); -} -function normalizeDbPath(dbPath) { - return normalizeLocalStoreDbPath(dbPath, resolveWorktreeAllocatorDbPath(), "invalid_worktree_allocator_db_path"); -} -function normalizeWorktreeBaseDir(worktreeBaseDir) { - const path = (worktreeBaseDir ?? resolveWorktreeBaseDir()).trim(); - if (!path) - throw new Error("invalid_worktree_base_dir"); - return path; -} -function normalizeMaxConcurrency(value) { - if (value === undefined || value === null) - return defaultMaxConcurrency; - if (!Number.isInteger(value) || value < 1) - throw new Error("invalid_max_concurrency"); - return value; -} -function normalizeMaxLeaseMs(value) { - if (value === undefined || value === null) - return DEFAULT_MAX_LEASE_MS; - if (!Number.isFinite(value) || value < 0) - throw new Error("invalid_max_lease_ms"); - return value; -} -function normalizeHostId(value) { - if (value === undefined || value === null) - return hostname(); - if (typeof value !== "string" || !value.trim()) - throw new Error("invalid_host_id"); - return value.trim(); -} -function normalizeRepoFullName(repoFullName) { - if (typeof repoFullName !== "string") - throw new Error("invalid_repo_full_name"); - const [owner, repo, extra] = repoFullName.trim().split("/"); - if (!owner || !repo || extra !== undefined) - throw new Error("invalid_repo_full_name"); - // #7525: extend #5831's path-safety guard here too — reject a `.`/`..`/control-char segment before it can - // be persisted into SQLite (or echoed back through the CLI), matching claim-ledger.ts's sibling parser. - if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) - throw new Error("invalid_repo_full_name"); - return `${owner}/${repo}`; -} -function normalizeAttemptId(attemptId) { - if (typeof attemptId !== "string") - throw new Error("invalid_attempt_id"); - const trimmed = attemptId.trim(); - if (!trimmed) - throw new Error("invalid_attempt_id"); - return trimmed; -} -export function isProcessAlive(pid) { - if (!Number.isInteger(pid) || pid <= 0) - return false; - try { - process.kill(pid, 0); - return true; - } - catch (error) { - // ESRCH = no such process; EPERM (or similar) means the process exists but we lack signal rights. - return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH" - ? false - : true; - } -} -function rowToAllocation(row) { - return { - slotIndex: row.slot_index, - worktreePath: row.worktree_path, - attemptId: row.attempt_id, - repoFullName: row.repo_full_name, - status: row.status, - ownerPid: row.owner_pid, - ownerHost: row.owner_host ?? null, - allocatedAt: row.allocated_at, - }; -} -function ensureSlotTable(db) { - db.exec(` - CREATE TABLE IF NOT EXISTS worktree_slots ( - slot_index INTEGER PRIMARY KEY, - worktree_path TEXT NOT NULL UNIQUE, - attempt_id TEXT UNIQUE, - repo_full_name TEXT, - status TEXT NOT NULL CHECK (status IN ('free', 'active')), - owner_pid INTEGER, - owner_host TEXT, - allocated_at TEXT - ) - `); - ensureOwnerHostColumn(db); -} -// Add the owner_host column (#7085) to an on-disk file created before it existed. `CREATE TABLE IF NOT EXISTS` -// above is a no-op against an already-existing table, so a pre-#7085 file needs this explicit ALTER — guarded by -// a presence check (same technique as attempt-log.js's ensureOutcomeColumns). A migrated row keeps owner_host -// NULL until its owner re-acquires, so the age-based reclaim (not the same-host pid fast path) governs it. -function ensureOwnerHostColumn(db) { - const hasOwnerHost = db - .prepare("PRAGMA table_info(worktree_slots)") - .all() - .some((column) => column.name === "owner_host"); - if (!hasOwnerHost) - db.exec("ALTER TABLE worktree_slots ADD COLUMN owner_host TEXT"); -} -function ensureSlots(db, worktreeBaseDir, maxConcurrency) { - mkdirSync(worktreeBaseDir, { recursive: true, mode: 0o700 }); - const insert = db.prepare(` - INSERT OR IGNORE INTO worktree_slots (slot_index, worktree_path, status) - VALUES (?, ?, 'free') - `); - for (let slotIndex = 0; slotIndex < maxConcurrency; slotIndex += 1) { - const worktreePath = join(worktreeBaseDir, `slot-${slotIndex}`); - insert.run(slotIndex, worktreePath); - mkdirSync(worktreePath, { recursive: true, mode: 0o700 }); - } -} -function allocationAgeMs(allocatedAt, nowMs) { - const allocatedMs = Date.parse(allocatedAt); - if (!Number.isFinite(allocatedMs)) - return null; - return nowMs - allocatedMs; -} -/** - * Decide whether an `active` slot is orphaned and should be reclaimed. Two independent signals: - * - Age (container-agnostic): a slot whose `allocated_at` is older than `maxLeaseMs` is reclaimed regardless of - * what `isProcessAlive` reports, guaranteeing eventual reclaim even when a cross-container caller observes the - * owner's pid in the wrong PID namespace. This is the only signal that is sound across fleet mode's separate - * containers, so it must never be gated behind the pid check. - * - Same-host pid liveness (fast path): only when the slot was leased by a process on THIS host (`owner_host` - * matches) is `isProcessAlive` a meaningful signal — a confirmed-dead (or missing) local owner frees its slot - * immediately without waiting out the lease. A foreign `owner_host` is never trusted for the pid check. - */ -function isSlotOrphaned(row, nowMs, maxLeaseMs, hostId) { - const ageMs = allocationAgeMs(row.allocated_at, nowMs); - if (ageMs !== null && ageMs > maxLeaseMs) - return true; - if (row.owner_host !== null && row.owner_host === hostId) { - return row.owner_pid === null || !isProcessAlive(row.owner_pid); - } - return false; -} -function reclaimOrphanedAllocations(db, nowMs, maxLeaseMs, hostId) { - const orphans = db - .prepare("SELECT slot_index, owner_pid, owner_host, allocated_at FROM worktree_slots WHERE status = 'active'") - .all(); - const reclaim = db.prepare(` - UPDATE worktree_slots - SET status = 'free', attempt_id = NULL, repo_full_name = NULL, owner_pid = NULL, owner_host = NULL, allocated_at = NULL - WHERE slot_index = ? - `); - for (const row of orphans) { - if (isSlotOrphaned(row, nowMs, maxLeaseMs, hostId)) - reclaim.run(row.slot_index); - } -} -/** - * Opens the local worktree allocator store. On startup reclaims orphaned active slots — any slot past its - * `maxLeaseMs` age (the container-agnostic guarantee for fleet mode's shared store), plus, as a same-host fast - * path, any slot whose owner pid is confirmed dead in THIS host's PID namespace. - */ -export function openWorktreeAllocator(options = {}) { - const resolvedPath = normalizeDbPath(options.dbPath); - const worktreeBaseDir = normalizeWorktreeBaseDir(options.worktreeBaseDir); - const maxConcurrency = normalizeMaxConcurrency(options.maxConcurrency); - const maxLeaseMs = normalizeMaxLeaseMs(options.maxLeaseMs); - const hostId = normalizeHostId(options.hostId); - const processPid = Number.isInteger(options.processPid) ? options.processPid : process.pid; - const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now(); - const db = openLocalStoreDb(resolvedPath); - ensureSlotTable(db); - ensureSlots(db, worktreeBaseDir, maxConcurrency); - reclaimOrphanedAllocations(db, nowMs, maxLeaseMs, hostId); - const getByAttempt = db.prepare("SELECT slot_index, worktree_path, attempt_id, repo_full_name, status, owner_pid, owner_host, allocated_at FROM worktree_slots WHERE attempt_id = ?"); - const countActive = db.prepare("SELECT COUNT(*) AS count FROM worktree_slots WHERE status = 'active'"); - const selectFreeSlot = db.prepare(` - SELECT slot_index, worktree_path, attempt_id, repo_full_name, status, owner_pid, owner_host, allocated_at - FROM worktree_slots - WHERE status = 'free' - ORDER BY slot_index - LIMIT 1 - `); - const markActive = db.prepare(` - UPDATE worktree_slots - SET status = 'active', attempt_id = ?, repo_full_name = ?, owner_pid = ?, owner_host = ?, allocated_at = ? - WHERE slot_index = ? - `); - const releaseByAttempt = db.prepare(` - UPDATE worktree_slots - SET status = 'free', attempt_id = NULL, repo_full_name = NULL, owner_pid = NULL, owner_host = NULL, allocated_at = NULL - WHERE attempt_id = ? AND status = 'active' - RETURNING slot_index, worktree_path, attempt_id, repo_full_name, status, owner_pid, owner_host, allocated_at - `); - const listSlots = db.prepare("SELECT slot_index, worktree_path, attempt_id, repo_full_name, status, owner_pid, owner_host, allocated_at FROM worktree_slots ORDER BY slot_index"); - const allocator = { - dbPath: resolvedPath, - worktreeBaseDir, - maxConcurrency, - maxLeaseMs, - processPid, - hostId, - acquire(attemptId, repoFullName) { - const normalizedAttempt = normalizeAttemptId(attemptId); - const normalizedRepo = normalizeRepoFullName(repoFullName); - const existing = getByAttempt.get(normalizedAttempt); - if (existing?.status === "active") - return rowToAllocation(existing); - db.exec("BEGIN IMMEDIATE"); - try { - const raced = getByAttempt.get(normalizedAttempt); - // In-transaction re-check: only reachable when another process activates the same attempt_id - // between the pre-BEGIN read and this transaction (covered by miner-worktree-allocator-collisions - // via child processes; those runs cannot attribute coverage back into this process). - /* v8 ignore next 4 -- multi-process race; see miner-worktree-allocator-collisions.test.ts */ - if (raced?.status === "active") { - db.exec("COMMIT"); - return rowToAllocation(raced); - } - const activeCount = countActive.get().count; - if (activeCount >= maxConcurrency) - throw new Error("worktree_capacity_exceeded"); - const slot = selectFreeSlot.get(); - if (!slot) - throw new Error("worktree_capacity_exceeded"); - const allocatedAt = new Date().toISOString(); - markActive.run(normalizedAttempt, normalizedRepo, processPid, hostId, allocatedAt, slot.slot_index); - db.exec("COMMIT"); - return rowToAllocation({ - ...slot, - attempt_id: normalizedAttempt, - repo_full_name: normalizedRepo, - status: "active", - owner_pid: processPid, - owner_host: hostId, - allocated_at: allocatedAt, - }); - } - catch (error) { - db.exec("ROLLBACK"); - throw error; - } - }, - release(attemptId) { - const normalizedAttempt = normalizeAttemptId(attemptId); - const row = releaseByAttempt.get(normalizedAttempt); - return row ? rowToAllocation(row) : null; - }, - listSlots() { - return listSlots.all().map(rowToAllocation); - }, - close() { - db.close(); - }, - }; - return allocator; -} -function getDefaultWorktreeAllocator() { - defaultWorktreeAllocator ??= openWorktreeAllocator(); - return defaultWorktreeAllocator; -} -export function acquireWorktree(attemptId, repoFullName) { - return getDefaultWorktreeAllocator().acquire(attemptId, repoFullName); -} -export function releaseWorktree(attemptId) { - return getDefaultWorktreeAllocator().release(attemptId); -} -export function closeDefaultWorktreeAllocator() { - if (!defaultWorktreeAllocator) - return; - defaultWorktreeAllocator.close(); - defaultWorktreeAllocator = null; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid29ya3RyZWUtYWxsb2NhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsid29ya3RyZWUtYWxsb2NhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtHQUErRztBQUMvRyw4R0FBOEc7QUFDOUcsd0RBQXdEO0FBQ3hELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDcEMsT0FBTyxFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDNUMsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUVqQyxPQUFPLEVBQUUseUJBQXlCLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN4RyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQTBEckQsTUFBTSxpQkFBaUIsR0FBRyw0QkFBNEIsQ0FBQztBQUN2RCxNQUFNLHNCQUFzQixHQUFHLFdBQVcsQ0FBQztBQUMzQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsQ0FBQztBQUNoQyxJQUFJLHdCQUF3QixHQUE2QixJQUFJLENBQUM7QUFFOUQsOEdBQThHO0FBQzlHLDJHQUEyRztBQUMzRyw4R0FBOEc7QUFDOUcsaUhBQWlIO0FBQ2pILGlIQUFpSDtBQUNqSCxzR0FBc0c7QUFDdEcsaUhBQWlIO0FBQ2pILGtIQUFrSDtBQUNsSCxvR0FBb0c7QUFDcEcsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDO0FBRXZELE1BQU0sVUFBVSw4QkFBOEIsQ0FBQyxNQUEwQyxPQUFPLENBQUMsR0FBRztJQUNsRyxPQUFPLHVCQUF1QixDQUFDLGlCQUFpQixFQUFFLHNDQUFzQyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0FBQ2pHLENBQUM7QUFFRCxNQUFNLFVBQVUsc0JBQXNCLENBQUMsTUFBMEMsT0FBTyxDQUFDLEdBQUc7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 \ No newline at end of file diff --git a/packages/loopover-miner/scripts/generate-env-reference.mjs b/packages/loopover-miner/scripts/generate-env-reference.mjs index 9ff6208fab..7b4e250bcd 100644 --- a/packages/loopover-miner/scripts/generate-env-reference.mjs +++ b/packages/loopover-miner/scripts/generate-env-reference.mjs @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { dirname, extname, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; -import { collectSelfHostEnvVars } from "../../../scripts/gen-selfhost-env-reference.mjs"; +import { collectSelfHostEnvVars } from "../../../scripts/gen-selfhost-env-reference.js"; export const DEFAULT_OUTPUT_PATH = "packages/loopover-miner/docs/env-reference.md"; export const DEFAULT_MODULE_OUTPUT_PATH = "apps/loopover-ui/src/lib/ams-env-reference.ts"; @@ -105,7 +105,7 @@ export function writeMinerEnvReference({ return { changed, outputPath, rows }; } -// Mirrors scripts/gen-selfhost-env-reference.mjs's renderSelfHostEnvReferenceModule -- same shape +// Mirrors scripts/gen-selfhost-env-reference.ts's renderSelfHostEnvReferenceModule -- same shape // (a typed rows array plus a pre-joined markdown string), so docs.ams-env-reference.tsx's content // can import AMS_ENV_REFERENCE_MARKDOWN and render it exactly the way // docs.self-hosting-configuration.tsx already renders SELFHOST_ENV_REFERENCE_MARKDOWN. The @@ -191,14 +191,25 @@ function sourceFiles(rootDir, sourceRoots) { return files; } +const COMPILED_JS_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); + function walkSourceFiles(dir) { const files = []; const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); + // Same reasoning as gen-selfhost-env-reference.ts's identical guard: a same-basename .ts/.tsx sibling + // means the .js is that source's compiled output (gitignored, built on demand since #7290/#7291/#7705), + // so whether it exists on disk varies by environment -- skip it so the collected DEFAULT_PATTERNS + // defaults don't silently depend on whether a build happened to run first. + const tsBasenames = new Set( + entries.filter((entry) => entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))).map((entry) => entry.name.slice(0, entry.name.lastIndexOf("."))), + ); for (const entry of entries) { const abs = resolve(dir, entry.name); if (entry.isDirectory()) { files.push(...walkSourceFiles(abs)); } else if (entry.isFile() && isSupportedSourceFile(abs) && !entry.name.endsWith(".d.ts")) { + const ext = extname(entry.name); + if (COMPILED_JS_EXTENSIONS.has(ext) && tsBasenames.has(entry.name.slice(0, -ext.length))) continue; files.push(abs); } } diff --git a/packages/loopover-miner/tsconfig.json b/packages/loopover-miner/tsconfig.json index 0f697159d5..fd26014e29 100644 --- a/packages/loopover-miner/tsconfig.json +++ b/packages/loopover-miner/tsconfig.json @@ -22,8 +22,8 @@ "tsBuildInfoFile": "./.tsbuildinfo" }, // Every bin/lib runtime module is real TypeScript (#7290 / #7317): tsc owns the in-place .js + .d.ts - // emit. The glob stays the include surface; test/unit/miner-typescript-migration-complete.test.ts - // fails closed if a hand-maintained .js/.d.ts orphan ever reappears. + // 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. "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. diff --git a/scripts/branding-drift-baseline.json b/scripts/branding-drift-baseline.json index bd7b54e7e5..b161c01de0 100644 --- a/scripts/branding-drift-baseline.json +++ b/scripts/branding-drift-baseline.json @@ -4,7 +4,6 @@ "apps/loopover-ui/src/routes/app.runs.tsx": 1, "apps/loopover-ui/src/routes/app.workbench.tsx": 1, "packages/loopover-engine/src/signals/engine.ts": 2, - "packages/loopover-mcp/bin/loopover-mcp.js": 2, "packages/loopover-mcp/bin/loopover-mcp.ts": 2, "src/api/routes.ts": 2, "src/db/repositories.ts": 1, diff --git a/scripts/build-extension.mjs b/scripts/build-extension.ts similarity index 93% rename from scripts/build-extension.mjs rename to scripts/build-extension.ts index c19a4667cd..ee59cee545 100644 --- a/scripts/build-extension.mjs +++ b/scripts/build-extension.ts @@ -2,7 +2,7 @@ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { createStoredZip } from "./extension-zip-core.mjs"; +import { createStoredZip } from "./extension-zip-core.js"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const source = resolve(root, "apps/loopover-extension"); diff --git a/scripts/build-miner-extension.mjs b/scripts/build-miner-extension.ts similarity index 94% rename from scripts/build-miner-extension.mjs rename to scripts/build-miner-extension.ts index 29bf0ef892..88e280c767 100644 --- a/scripts/build-miner-extension.mjs +++ b/scripts/build-miner-extension.ts @@ -2,7 +2,7 @@ import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { createStoredZip } from "./extension-zip-core.mjs"; +import { createStoredZip } from "./extension-zip-core.js"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const source = resolve(root, "apps/loopover-miner-extension"); diff --git a/scripts/check-branding-drift.d.mts b/scripts/check-branding-drift.d.mts deleted file mode 100644 index a91efa047f..0000000000 --- a/scripts/check-branding-drift.d.mts +++ /dev/null @@ -1,9 +0,0 @@ -export const BASELINE_RELATIVE_PATH: string; -export const BRANDING_DRIFT_PATHSPECS: string[]; - -export function scanBrandingHits(options: { - root: string; - exec?: (root: string, args: string[]) => string; -}): Record; - -export function diffBrandingBaseline(baseline: Record, current: Record): string[]; diff --git a/scripts/check-branding-drift.mjs b/scripts/check-branding-drift.ts similarity index 90% rename from scripts/check-branding-drift.mjs rename to scripts/check-branding-drift.ts index 4ba7177466..a11bf6d152 100644 --- a/scripts/check-branding-drift.mjs +++ b/scripts/check-branding-drift.ts @@ -43,12 +43,12 @@ export const BRANDING_DRIFT_PATHSPECS = [ ":(exclude)packages/*/test/**", ]; -function defaultExec(root, args) { +function defaultExec(root: string, args: string[]): string { try { return execFileSync("git", args, { cwd: root, encoding: "utf8" }); } catch (error) { // git grep exits 1 for "zero matches" -- not a real failure, just an empty result. - if (error.status === 1) return ""; + if ((error as NodeJS.ErrnoException & { status?: number }).status === 1) return ""; throw error; } } @@ -57,9 +57,9 @@ function defaultExec(root, args) { * count (line-granularity, not raw occurrence count -- sufficient to detect "something new appeared" without * the fragility of an exact substring-occurrence count). Uses `git grep`, so it only ever sees tracked files * exactly as CI would check them out -- no node_modules/dist/build noise to exclude by hand. */ -export function scanBrandingHits({ root, exec = defaultExec }) { +export function scanBrandingHits({ root, exec = defaultExec }: { root: string; exec?: typeof defaultExec }): Record { const output = exec(root, ["grep", "-ciI", "gittensory", "--", ...BRANDING_DRIFT_PATHSPECS]); - const counts = {}; + const counts: Record = {}; for (const line of output.split("\n")) { if (!line) continue; const separatorIndex = line.lastIndexOf(":"); @@ -72,8 +72,8 @@ export function scanBrandingHits({ root, exec = defaultExec }) { /** Pure comparison: every failure is one of "increased" (new drift -- or a file that didn't exist in the * baseline at all, same failure shape) or "decreased" (a cleanup landed; still a failure so the baseline * never silently drifts stale, but a one-line fix -- regenerate). Sorted for stable, reviewable CI output. */ -export function diffBrandingBaseline(baseline, current) { - const failures = []; +export function diffBrandingBaseline(baseline: Record, current: Record): string[] { + const failures: string[] = []; const files = new Set([...Object.keys(baseline), ...Object.keys(current)]); for (const file of [...files].sort()) { const before = baseline[file] ?? 0; @@ -91,11 +91,11 @@ export function diffBrandingBaseline(baseline, current) { return failures; } -function readBaseline(root) { +function readBaseline(root: string): Record { return JSON.parse(readFileSync(join(root, BASELINE_RELATIVE_PATH), "utf8")); } -function writeBaseline(root, counts) { +function writeBaseline(root: string, counts: Record): void { const sorted = Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b))); writeFileSync(join(root, BASELINE_RELATIVE_PATH), `${JSON.stringify(sorted, null, 2)}\n`); } diff --git a/scripts/check-build-drift.d.mts b/scripts/check-build-drift.d.mts deleted file mode 100644 index a04bbb4384..0000000000 --- a/scripts/check-build-drift.d.mts +++ /dev/null @@ -1,8 +0,0 @@ -export function pathsForPackage(name: string): string[]; - -export function checkBuildDrift( - paths: string[], - options?: { cwd?: string; run?: (paths: string[], cwd: string) => string }, -): string; - -export function main(argv: string[]): void; diff --git a/scripts/check-build-drift.mjs b/scripts/check-build-drift.mjs deleted file mode 100644 index cdee307a32..0000000000 --- a/scripts/check-build-drift.mjs +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node -// packages/loopover-{miner,mcp} both compile real TypeScript in place (tsc's outDir === rootDir, so -// e.g. lib/foo.ts emits lib/foo.js + lib/foo.d.ts right next to it) and commit that emitted output -- -// both packages ship as installable CLIs (npm install -g), so a runnable .js has to exist in the -// published tarball, and root-level tests import the emitted .js by its literal specifier (NodeNext -// resolution), not the .ts source, so a stale commit makes test:coverage silently exercise old -// behavior instead of loudly failing. package.json composes this AFTER the package's real build -// (`npm run build:{miner,mcp} && node scripts/check-build-drift.mjs {miner,mcp}`) -- this script's own -// job is just: did that build change anything relative to what's committed. Mirrors cf-typegen:check's -// "regenerate for real, then diff" shape for a package whose generated output is emitted in place -// across a whole directory rather than to one named file. -import { execFileSync } from "node:child_process"; -import { fileURLToPath } from "node:url"; - -const PACKAGE_PATHS = { - miner: ["packages/loopover-miner/bin", "packages/loopover-miner/lib"], - mcp: ["packages/loopover-mcp/bin", "packages/loopover-mcp/lib"], -}; - -/** The bin/lib directories a package name checks. Exported (rather than inlined into main()) so a test - * can validate the real PACKAGE_PATHS table itself -- e.g. that "miner" resolves to real repo paths -- - * not just a fake stand-in for it. Throws on an unknown name instead of silently checking nothing. */ -export function pathsForPackage(name) { - const paths = PACKAGE_PATHS[name]; - if (!paths) { - throw new Error(`check-build-drift: unknown package "${name}" (expected one of: ${Object.keys(PACKAGE_PATHS).join(", ")})`); - } - return paths; -} - -function defaultGitStatus(paths, cwd) { - return execFileSync("git", ["status", "--porcelain", "--", ...paths], { cwd, encoding: "utf8" }); -} - -/** Trimmed `git status --porcelain` text for `paths` under `cwd` (a real git worktree) -- "" when - * clean. Porcelain (not plain `git diff --exit-code`) deliberately: a .ts file whose emitted .js/.d.ts - * was never committed at all shows up as an untracked file, which `git diff` alone never reports. - * `run` is injectable so tests can fake the git call entirely; the default shells out for real. */ -export function checkBuildDrift(paths, { cwd = process.cwd(), run = defaultGitStatus } = {}) { - return run(paths, cwd).trim(); -} - -export function main(argv) { - try { - const name = argv[0]; - const paths = pathsForPackage(name); - const drift = checkBuildDrift(paths); - if (drift.length > 0) { - process.stderr.write( - `check-build-drift: packages/loopover-${name}'s committed .js/.d.ts is stale relative to its .ts source -- run \`npm run build:${name}\` and commit the result:\n${drift}\n`, - ); - process.exit(1); - return; - } - process.stdout.write(`check-build-drift: packages/loopover-${name}'s committed .js/.d.ts matches its .ts source.\n`); - } catch (err) { - process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); - process.exit(1); - } -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2)); diff --git a/scripts/check-docs-drift.d.mts b/scripts/check-docs-drift.d.mts deleted file mode 100644 index 5dffd8a5d6..0000000000 --- a/scripts/check-docs-drift.d.mts +++ /dev/null @@ -1,33 +0,0 @@ -export function extractLoopOverReviewFlags(envDtsText: string): string[]; - -export function extractCatalogIds(sourceText: string, catalogConstName: string): string[]; - -export function extractGateModeFields(typesText: string): string[]; - -export function extractRepositorySettingsFields(typesText: string): string[]; - -export function extractFocusManifestFields(focusManifestText: string): string[]; - -export type GateModeManifestRow = { field: string; aliases: string[]; pages: string[] }; - -export const GATE_MODE_MANIFEST: GateModeManifestRow[]; - -export type AliasManifestRow = { field: string; aliases: string[] }; - -export const SETTINGS_ALIAS_MANIFEST: AliasManifestRow[]; - -export const FOCUS_MANIFEST_ALIAS_MANIFEST: AliasManifestRow[]; - -export function checkDocsDrift(options: { - root: string; - readFile?: (root: string, relativePath: string) => string; -}): { - failures: string[]; - counts: { - flags: number; - commands: number; - gateModes: number; - settingsFields: number; - focusManifestFields: number; - }; -}; diff --git a/scripts/check-docs-drift.mjs b/scripts/check-docs-drift.ts similarity index 91% rename from scripts/check-docs-drift.mjs rename to scripts/check-docs-drift.ts index 223d56d8b4..be2d5eb908 100644 --- a/scripts/check-docs-drift.mjs +++ b/scripts/check-docs-drift.ts @@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url"; * shape (optional `?`, then `:`, then whitespace, then `string`) rather than a bare name match, so a comment * that merely MENTIONS a flag name (common in this file's prose-heavy JSDoc) is never mistaken for a real * declaration. */ -export function extractLoopOverReviewFlags(envDtsText) { +export function extractLoopOverReviewFlags(envDtsText: string): string[] { const matches = envDtsText.matchAll(/LOOPOVER_REVIEW_[A-Z0-9_]+(?=\??:\s*string)/g); return [...new Set([...matches].map((match) => match[0]))]; } @@ -28,12 +28,12 @@ export function extractLoopOverReviewFlags(envDtsText) { * inside themselves, so the first close is always the right one) and extract every `id: ""` string * from within that slice. Scoped to the named catalog's own slice so two catalogs in the same file never * bleed into each other's id list. */ -export function extractCatalogIds(sourceText, catalogConstName) { +export function extractCatalogIds(sourceText: string, catalogConstName: string): string[] { const catalogPattern = new RegExp(`const\\s+${catalogConstName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s*const;`); const catalogMatch = catalogPattern.exec(sourceText); if (!catalogMatch) return []; - const idMatches = catalogMatch[1].matchAll(/id:\s*"([^"]+)"/g); - return [...new Set([...idMatches].map((match) => match[1]))]; + const idMatches = catalogMatch[1]!.matchAll(/id:\s*"([^"]+)"/g); + return [...new Set([...idMatches].map((match) => match[1]!))]; } /** Extract every unique identifier matching `[a-zA-Z]+GateMode` DECLARED as a field (optional `?` then `:`) @@ -41,11 +41,13 @@ export function extractCatalogIds(sourceText, catalogConstName) { * Anchored on the field-declaration shape so a comment mentioning a GateMode name in prose (this file's * JSDoc references sibling gate modes constantly, e.g. "mirrors sizeGateMode") is never mistaken for a real * field. */ -export function extractGateModeFields(typesText) { +export function extractGateModeFields(typesText: string): string[] { const matches = typesText.matchAll(/[a-zA-Z]+GateMode(?=\??:)/g); return [...new Set([...matches].map((match) => match[0]))]; } +type TypeLiteralFieldEntry = { name: string; typeText: string }; + /** Shared brace-depth-slicing field extractor for any `export type = { ... };` object-literal in * `text`. Walks forward from the matching open brace counting `{`/`}` so the type's OWN closing brace is found * regardless of nested object/generic braces inside a field's type (e.g. `ReadonlyArray<{ model: string; ... @@ -57,7 +59,7 @@ export function extractGateModeFields(typesText) { * declaration order -- `typeText` is the field's own declared type (the text after its `:` up to its * terminating top-level `;`), used by `extractFocusManifestFields` to detect a bare reference to another local * type worth recursing into. */ -function extractTypeLiteralFieldEntries(text, typeName) { +function extractTypeLiteralFieldEntries(text: string, typeName: string): TypeLiteralFieldEntry[] | null { const declPattern = new RegExp(`export type ${typeName}\\s*=\\s*\\{`); const declMatch = declPattern.exec(text); if (!declMatch) return null; @@ -69,10 +71,10 @@ function extractTypeLiteralFieldEntries(text, typeName) { else if (text[index] === "}") depth--; } const body = text.slice(bodyStart, index - 1); - const fields = []; + const fields: TypeLiteralFieldEntry[] = []; for (const line of body.split("\n")) { const fieldMatch = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\??:\s*(.+);\s*$/.exec(line); - if (fieldMatch) fields.push({ name: fieldMatch[1], typeText: fieldMatch[2].trim() }); + if (fieldMatch) fields.push({ name: fieldMatch[1]!, typeText: fieldMatch[2]!.trim() }); } return fields; } @@ -85,7 +87,7 @@ function extractTypeLiteralFieldEntries(text, typeName) { * order -- judging which fields are genuinely yml-configurable vs internal bookkeeping is `checkDocsDrift`'s * job (`NOT_YML_CONFIGURABLE_SETTINGS_FIELDS` below), mirroring how `extractGateModeFields` also returns every * match unfiltered and leaves the GATE_MODE_MANIFEST cross-reference to the caller. */ -export function extractRepositorySettingsFields(typesText) { +export function extractRepositorySettingsFields(typesText: string): string[] { return (extractTypeLiteralFieldEntries(typesText, "RepositorySettings") ?? []).map((entry) => entry.name); } @@ -102,6 +104,8 @@ export function extractRepositorySettingsFields(typesText) { * `*GateMode`-shaped field, so it's held to the same yml-exhaustiveness bar as the rest of the surface now. */ const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set(["repoFullName", "createdAt", "updatedAt"]); +export type AliasManifestRow = { field: string; aliases: string[] }; + /** RepositorySettings fields whose `.loopover.yml.example` documentation exists under a DIFFERENT, shorter * name than the field itself -- almost always because the yml groups several sibling fields under one named * block (`gate.aiReview.*`, `gate.cla.*`, `gate.slop.*`, `gate.copycat.*`, `gate.readiness.*`) and so drops the @@ -113,7 +117,7 @@ const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set(["repoFullName", "createdAt * against the WHOLE `.loopover.yml.example` file, matching #4617's "SOME mention" ask, so one representative * alias per row is enough. Any `*GateMode` field is deliberately absent from this manifest even though its own * yml key is ALSO renamed the same way -- GATE_MODE_MANIFEST above already owns that exhaustive cross-check. */ -export const SETTINGS_ALIAS_MANIFEST = [ +export const SETTINGS_ALIAS_MANIFEST: AliasManifestRow[] = [ { field: "reviewCheckMode", aliases: ["checkMode"] }, { field: "gatePack", aliases: ["pack:"] }, { field: "qualityGateMinScore", aliases: ["minScore"] }, @@ -144,7 +148,7 @@ export const SETTINGS_ALIAS_MANIFEST = [ * Every OTHER FocusManifest-reachable block keeps its source field's camelCase spelling verbatim in the yml * (matching the top-level manifest fields and the `gate:`/`settings:` blocks), for which this is a harmless * no-op: a name with no lower-to-upper case boundary is unchanged by the transform. */ -function toSnakeCase(name) { +function toSnakeCase(name: string): string { return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(); } @@ -177,11 +181,11 @@ const FOCUS_MANIFEST_SKIP_TOP_LEVEL_FIELDS = new Set(["gate", "settings"]); * Record/generic (e.g. `pathInstructions: ReviewPathInstruction[]`, `fields: Partial>`) is treated as ONE leaf itself rather than recursed into -- it is documented (or not) as a single * structured knob, matching how the rest of this script treats `aiReviewReviewers`'s array-of-objects shape. */ -export function extractFocusManifestFields(focusManifestText) { - const leaves = []; - const visitedTypes = new Set(); +export function extractFocusManifestFields(focusManifestText: string): string[] { + const leaves: string[] = []; + const visitedTypes = new Set(); - function walk(typeName, pathPrefix) { + function walk(typeName: string, pathPrefix: readonly string[]) { if (visitedTypes.has(typeName)) return; // guards a hypothetical future cycle; no real cycle exists today visitedTypes.add(typeName); const entries = extractTypeLiteralFieldEntries(focusManifestText, typeName); @@ -208,19 +212,21 @@ export function extractFocusManifestFields(focusManifestText) { * tag would suggest -- e.g. `review.footerText`'s own field carries no `` `review.footer.text` `` tag at all * (unlike most of its siblings), and is in fact documented as just `footer:` (a nested `text:` sub-key). * Mirrors SETTINGS_ALIAS_MANIFEST's reasoning exactly, one level down. */ -export const FOCUS_MANIFEST_ALIAS_MANIFEST = [ +export const FOCUS_MANIFEST_ALIAS_MANIFEST: AliasManifestRow[] = [ { field: "review.footerText", aliases: ["footer:"] }, { field: "review.enrichmentAnalyzers", aliases: ["enrichment:"] }, { field: "review.reviewMemory", aliases: ["memory:"] }, ]; +export type GateModeManifestRow = { field: string; aliases: string[]; pages: string[] }; + // The real current *GateMode fields on RepositorySettings in src/types.ts. Each row maps the field to its // .loopover.yml alias(es) (the field's own DB/settings name, plus any config-as-code YAML path it is also // known by) and the docs content filenames (relative to apps/loopover-ui/content/docs/) that must document it. // Adding a new *GateMode field to src/types.ts without adding a row here is a docs-drift failure by design // (see checkDocsDrift step 3) -- the manifest is the single place that maps "a gate dimension exists" to // "here is where a maintainer can read about it". -export const GATE_MODE_MANIFEST = [ +export const GATE_MODE_MANIFEST: GateModeManifestRow[] = [ { field: "linkedIssueGateMode", aliases: ["linkedIssueGateMode", "gate.linkedIssue"], pages: ["how-reviews-work.mdx", "tuning.mdx"] }, { field: "duplicatePrGateMode", aliases: ["duplicatePrGateMode", "gate.duplicates"], pages: ["how-reviews-work.mdx", "tuning.mdx"] }, { field: "qualityGateMode", aliases: ["qualityGateMode", "gate.readiness.mode"], pages: ["how-reviews-work.mdx", "tuning.mdx"] }, @@ -239,10 +245,26 @@ export const GATE_MODE_MANIFEST = [ const DOCS_ROUTES_DIR = "apps/loopover-ui/content/docs"; -function defaultReadFile(root, relativePath) { +function defaultReadFile(root: string, relativePath: string): string { return readFileSync(join(root, relativePath), "utf8"); } +export type CheckDocsDriftOptions = { + root: string; + readFile?: (root: string, relativePath: string) => string; +}; + +export type CheckDocsDriftResult = { + failures: string[]; + counts: { + flags: number; + commands: number; + gateModes: number; + settingsFields: number; + focusManifestFields: number; + }; +}; + /** * Cross-check feature flags, @loopover commands, gate-mode dimensions, the full RepositorySettings surface, * and every parseable FocusManifest field between their code source of truth and wherever they're meant to be @@ -251,9 +273,9 @@ function defaultReadFile(root, relativePath) { * page or source file without touching the real filesystem. Returns `{ failures, counts }` -- pure given its * inputs, no process.exit/console side effects of its own (those live in main()). */ -export function checkDocsDrift({ root, readFile = defaultReadFile }) { - const failures = []; - const read = (relativePath) => readFile(root, relativePath); +export function checkDocsDrift({ root, readFile = defaultReadFile }: CheckDocsDriftOptions): CheckDocsDriftResult { + const failures: string[] = []; + const read = (relativePath: string) => readFile(root, relativePath); // 1. Feature flags: src/env.d.ts vs tuning.mdx + privacy-security.mdx. const envDtsText = read("src/env.d.ts"); @@ -306,7 +328,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { const manifestFields = new Set(GATE_MODE_MANIFEST.map((row) => row.field)); for (const field of gateModeFields) { if (!manifestFields.has(field)) { - failures.push(`src/types.ts declares ${field} but GATE_MODE_MANIFEST in scripts/check-docs-drift.mjs has no entry for it -- add a row mapping it to its .loopover.yml alias(es) and the docs pages that must document it`); + failures.push(`src/types.ts declares ${field} but GATE_MODE_MANIFEST in scripts/check-docs-drift.ts has no entry for it -- add a row mapping it to its .loopover.yml alias(es) and the docs pages that must document it`); } } @@ -342,7 +364,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { const aliases = settingsAliases.get(field); if (aliases?.some((alias) => ymlExampleText.includes(alias))) continue; failures.push( - `.loopover.yml.example: missing any mention of RepositorySettings field "${field}" -- document it there (or the relevant reference doc), or add a SETTINGS_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, + `.loopover.yml.example: missing any mention of RepositorySettings field "${field}" -- document it there (or the relevant reference doc), or add a SETTINGS_ALIAS_MANIFEST row in scripts/check-docs-drift.ts if it's already documented under a different yml key name`, ); } } @@ -360,14 +382,14 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) { const ymlExampleText = read(".loopover.yml.example"); for (const path of focusManifestFields) { const segments = path.split("."); - const leaf = segments[segments.length - 1]; + const leaf = segments[segments.length - 1]!; const snakeLeaf = toSnakeCase(leaf); if (ymlExampleText.includes(leaf) || ymlExampleText.includes(snakeLeaf)) continue; const aliases = focusManifestAliases.get(path); if (aliases?.some((alias) => ymlExampleText.includes(alias))) continue; const prettyPath = segments.map(toSnakeCase).join("."); failures.push( - `.loopover.yml.example: missing any mention of FocusManifest field "${prettyPath}" -- document it there, or add a FOCUS_MANIFEST_ALIAS_MANIFEST row in scripts/check-docs-drift.mjs if it's already documented under a different yml key name`, + `.loopover.yml.example: missing any mention of FocusManifest field "${prettyPath}" -- document it there, or add a FOCUS_MANIFEST_ALIAS_MANIFEST row in scripts/check-docs-drift.ts if it's already documented under a different yml key name`, ); } } diff --git a/scripts/check-engines-nvmrc-sync.d.mts b/scripts/check-engines-nvmrc-sync.d.mts deleted file mode 100644 index def6465357..0000000000 --- a/scripts/check-engines-nvmrc-sync.d.mts +++ /dev/null @@ -1,9 +0,0 @@ -export function checkEnginesNvmrcSync(options: { - root: string; - readFile?: (root: string, relativePath: string) => string; - listDir?: (root: string, relativePath: string) => string[]; -}): { - failures: string[]; - nvmrcMajor: number; - checkedPackages: string[]; -}; diff --git a/scripts/check-engines-nvmrc-sync.mjs b/scripts/check-engines-nvmrc-sync.ts similarity index 80% rename from scripts/check-engines-nvmrc-sync.mjs rename to scripts/check-engines-nvmrc-sync.ts index 48fd157228..c9dd8dccac 100644 --- a/scripts/check-engines-nvmrc-sync.mjs +++ b/scripts/check-engines-nvmrc-sync.ts @@ -16,11 +16,11 @@ const ROOT_PACKAGE_JSON = "package.json"; const NVMRC_PATH = ".nvmrc"; const WORKSPACE_GROUPS = ["apps", "packages"]; -function defaultReadFile(root, relativePath) { +function defaultReadFile(root: string, relativePath: string): string { return readFileSync(join(root, relativePath), "utf8"); } -function defaultListDir(root, relativePath) { +function defaultListDir(root: string, relativePath: string): string[] { return readdirSync(join(root, relativePath), { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name); @@ -29,7 +29,7 @@ function defaultListDir(root, relativePath) { /** The .nvmrc pin is a bare major (e.g. "22"), not a full x.y.z -- nvm and actions/setup-node both * resolve it to the latest available patch of that major, so the only thing every engines.node range * must agree on is which major that is. */ -function parseNvmrcMajor(nvmrcContent) { +function parseNvmrcMajor(nvmrcContent: string): number { const trimmed = nvmrcContent.trim().replace(/^v/, ""); const major = Number.parseInt(trimmed, 10); if (!Number.isInteger(major) || major <= 0) { @@ -38,10 +38,10 @@ function parseNvmrcMajor(nvmrcContent) { return major; } -function findWorkspacePackageJsonPaths({ root, listDir }) { - const paths = []; +function findWorkspacePackageJsonPaths({ root, listDir }: { root: string; listDir: typeof defaultListDir }): string[] { + const paths: string[] = []; for (const group of WORKSPACE_GROUPS) { - let entries; + let entries: string[]; try { entries = listDir(root, group); } catch { @@ -52,20 +52,28 @@ function findWorkspacePackageJsonPaths({ root, listDir }) { return paths; } -/** - * @param {{ root: string, readFile?: typeof defaultReadFile, listDir?: typeof defaultListDir }} args - * @returns {{ failures: string[], nvmrcMajor: number, checkedPackages: string[] }} - */ -export function checkEnginesNvmrcSync({ root, readFile = defaultReadFile, listDir = defaultListDir }) { +export function checkEnginesNvmrcSync({ + root, + readFile = defaultReadFile, + listDir = defaultListDir, +}: { + root: string; + readFile?: typeof defaultReadFile; + listDir?: typeof defaultListDir; +}): { + failures: string[]; + nvmrcMajor: number; + checkedPackages: string[]; +} { const nvmrcMajor = parseNvmrcMajor(readFile(root, NVMRC_PATH)); const nextMajorFloor = `${nvmrcMajor + 1}.0.0`; const candidatePaths = [ROOT_PACKAGE_JSON, ...findWorkspacePackageJsonPaths({ root, listDir })]; - const failures = []; - const checkedPackages = []; + const failures: string[] = []; + const checkedPackages: string[] = []; for (const packageJsonPath of candidatePaths) { - let manifest; + let manifest: { engines?: { node?: string } }; try { manifest = JSON.parse(readFile(root, packageJsonPath)); } catch { @@ -100,9 +108,7 @@ function main() { process.exit(1); } - console.log( - `Engines/.nvmrc sync check ok: ${checkedPackages.length} package(s) with an engines.node pin all exclude Node ${nvmrcMajor + 1}+.`, - ); + console.log(`Engines/.nvmrc sync check ok: ${checkedPackages.length} package(s) with an engines.node pin all exclude Node ${nvmrcMajor + 1}+.`); } // Guard so importing this module for its pure exports (tests) never triggers the file-read/exit side effects. diff --git a/scripts/check-manifest-drift.d.mts b/scripts/check-manifest-drift.d.mts deleted file mode 100644 index b88950a4d1..0000000000 --- a/scripts/check-manifest-drift.d.mts +++ /dev/null @@ -1,9 +0,0 @@ -export function checkManifestDrift(options: { - root: string; - readFile?: (root: string, relativePath: string) => string; - bundledYaml?: string; -}): { - failures: string[]; - rootManifest: unknown; - bundledManifest: unknown; -}; diff --git a/scripts/check-manifest-drift.mjs b/scripts/check-manifest-drift.ts similarity index 84% rename from scripts/check-manifest-drift.mjs rename to scripts/check-manifest-drift.ts index f72ecfcf4a..0ff4d79131 100644 --- a/scripts/check-manifest-drift.mjs +++ b/scripts/check-manifest-drift.ts @@ -10,23 +10,23 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { parse as parseYaml } from "yaml"; -import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML } from "../src/config/loopover-repo-focus-manifest.ts"; +import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML } from "../src/config/loopover-repo-focus-manifest.js"; const ROOT_MANIFEST_PATH = ".loopover.yml"; -function defaultReadFile(root, relativePath) { +function defaultReadFile(root: string, relativePath: string): string { return readFileSync(join(root, relativePath), "utf8"); } /** Recursively sort object keys so deep-equal comparisons never depend on key insertion order (YAML key * order is not semantically meaningful; a reordered-but-equivalent block must not be reported as drift). */ -function sortKeysDeep(value) { +function sortKeysDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortKeysDeep); if (value !== null && typeof value === "object") { return Object.fromEntries( Object.keys(value) .sort() - .map((key) => [key, sortKeysDeep(value[key])]), + .map((key) => [key, sortKeysDeep((value as Record)[key])]), ); } return value; @@ -39,8 +39,17 @@ function sortKeysDeep(value) { * bundledManifest }` -- pure given its inputs, no process.exit/console side effects of its own (those live * in main()). */ -export function checkManifestDrift({ root, readFile = defaultReadFile, bundledYaml = LOOPOVER_REPO_FOCUS_MANIFEST_YAML }) { - const failures = []; +export function checkManifestDrift(options: { + root: string; + readFile?: (root: string, relativePath: string) => string; + bundledYaml?: string; +}): { + failures: string[]; + rootManifest: unknown; + bundledManifest: unknown; +} { + const { root, readFile = defaultReadFile, bundledYaml = LOOPOVER_REPO_FOCUS_MANIFEST_YAML } = options; + const failures: string[] = []; const rootManifestText = readFile(root, ROOT_MANIFEST_PATH); const rootManifest = parseYaml(rootManifestText); diff --git a/scripts/check-mcp-package.mjs b/scripts/check-mcp-package.mjs index c16d0bf836..e0abe2ece8 100644 --- a/scripts/check-mcp-package.mjs +++ b/scripts/check-mcp-package.mjs @@ -3,8 +3,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs"; -import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.mjs"; +import { FORBIDDEN_CONTENT } from "./forbidden-content.js"; +import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.js"; const FORBIDDEN_PATH = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; const STALE_PACKAGE_TEXT = /(private beta|zeronode\.workers\.dev|preview URL)/i; diff --git a/scripts/check-mcp-release-candidate.mjs b/scripts/check-mcp-release-candidate.mjs index a8688c5263..a5c1796225 100644 --- a/scripts/check-mcp-release-candidate.mjs +++ b/scripts/check-mcp-release-candidate.mjs @@ -10,7 +10,7 @@ import { checkTokenlessPublish, expectedReleaseTag, redactSensitive, -} from "./mcp-release-candidate-core.mjs"; +} from "./mcp-release-candidate-core.js"; const PACKAGE_DIR = "packages/loopover-mcp"; const WORKSPACE = "@loopover/mcp"; diff --git a/scripts/check-mcp-release-due.d.mts b/scripts/check-mcp-release-due.d.mts deleted file mode 100644 index 1cb2c67a93..0000000000 --- a/scripts/check-mcp-release-due.d.mts +++ /dev/null @@ -1,13 +0,0 @@ -import type { McpReleaseReport } from "./mcp-release-core.d.mts"; - -export type GitHubIssueCandidate = { - pull_request?: unknown; - title?: unknown; - body?: unknown; - user?: { - login?: unknown; - } | null; -}; - -export function isReleaseWatchIssue(issue: GitHubIssueCandidate): boolean; -export function closeResolvedIssueIfPresent(report: McpReleaseReport): Promise; diff --git a/scripts/check-mcp-release-due.mjs b/scripts/check-mcp-release-due.ts similarity index 79% rename from scripts/check-mcp-release-due.mjs rename to scripts/check-mcp-release-due.ts index 01b2dd7917..c9a4d59e70 100644 --- a/scripts/check-mcp-release-due.mjs +++ b/scripts/check-mcp-release-due.ts @@ -1,17 +1,35 @@ import { execFileSync, spawnSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; -import { buildMcpReleaseIssue, buildMcpReleaseReport, latestSemverTag, MCP_RELEASE_DUE_MARKER } from "./mcp-release-core.mjs"; +import { buildMcpReleaseIssue, buildMcpReleaseReport, latestSemverTag, MCP_RELEASE_DUE_MARKER, type McpReleaseCommit, type McpReleaseReport } from "./mcp-release-core.js"; const packageJsonPath = "packages/loopover-mcp/package.json"; // Per-request timeout so a hung api.github.com connection can't block the unattended mcp-release-watch job // indefinitely, matching the connection guard sibling release/observability scripts already use (#7014). const GITHUB_REQUEST_TIMEOUT_MS = 30_000; +type ParsedArgs = { + json: boolean; + output: string | null; + upsertIssue: boolean; +}; + +export type GitHubIssueCandidate = { + pull_request?: unknown; + title?: unknown; + body?: unknown; + user?: { + login?: unknown; + } | null; +}; + async function main() { const args = parseArgs(process.argv.slice(2)); - const latestTag = latestSemverTag(git(["tag", "--list", "mcp-v*", "--sort=-v:refname"]).split("\n").filter(Boolean)); + const latestTag = latestSemverTag( + git(["tag", "--list", "mcp-v*", "--sort=-v:refname"]) + .split("\n") + .filter(Boolean), + ); const packageVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version; const publishedVersion = readPublishedPackageVersion(); const commits = latestTag ? readCommits(`${latestTag.tag}..HEAD`) : readCommits("HEAD"); @@ -31,14 +49,14 @@ async function main() { } } -function parseArgs(argv) { - const args = { json: false, output: null, upsertIssue: false }; +function parseArgs(argv: readonly string[]): ParsedArgs { + const args: ParsedArgs = { json: false, output: null, upsertIssue: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--json") { args.json = true; } else if (arg === "--output") { - args.output = argv[++index]; + args.output = argv[++index] ?? null; } else if (arg === "--upsert-issue") { args.upsertIssue = true; } else { @@ -48,7 +66,7 @@ function parseArgs(argv) { return args; } -function readPublishedPackageVersion() { +function readPublishedPackageVersion(): string | null { const result = spawnSync("npm", ["view", "@loopover/mcp", "version", "--json"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); if (result.status !== 0) return null; try { @@ -59,7 +77,7 @@ function readPublishedPackageVersion() { } } -function readCommits(revisionRange) { +function readCommits(revisionRange: string): McpReleaseCommit[] { const format = "%x1e%H%x1f%s%x1f%B"; const logOutput = git(["log", "--reverse", "--no-merges", `--format=${format}`, revisionRange]); return logOutput @@ -69,23 +87,25 @@ function readCommits(revisionRange) { .map((entry) => { const [sha, subject, ...bodyParts] = entry.split("\x1f"); return { - sha, + sha: sha!, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), - files: readCommitFiles(sha), + files: readCommitFiles(sha!), }; }); } -function readCommitFiles(sha) { - return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]).split("\n").filter(Boolean); +function readCommitFiles(sha: string): string[] { + return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]) + .split("\n") + .filter(Boolean); } -function git(args) { +function git(args: readonly string[]): string { return execFileSync("git", args, { encoding: "utf8", maxBuffer: 1024 * 1024 * 200 }); } -async function upsertIssue(report) { +async function upsertIssue(report: McpReleaseReport) { const { owner, repo, token } = resolveRepoAndToken(); const issue = buildMcpReleaseIssue(report); const existingIssue = await findExistingIssue({ owner, repo, token }); @@ -112,7 +132,7 @@ async function upsertIssue(report) { // upsertIssue only ever opens/refreshes the tracking issue while a release is due -- without this, // the issue it created is never closed once release-please catches up (confirmed live: #6145 stayed // open and stale after mcp-v3.1.0's release-please PR merged, since no due=false run had ever closed it). -export async function closeResolvedIssueIfPresent(report) { +export async function closeResolvedIssueIfPresent(report: McpReleaseReport) { const { owner, repo, token } = resolveRepoAndToken(); const existingIssue = await findExistingIssue({ owner, repo, token }); if (!existingIssue) return; @@ -133,7 +153,7 @@ export async function closeResolvedIssueIfPresent(report) { process.stdout.write(`Closed issue #${existingIssue.number}: release caught up.\n`); } -function resolveRepoAndToken() { +function resolveRepoAndToken(): { owner: string; repo: string; token: string } { const repository = process.env.GITHUB_REPOSITORY; const token = process.env.GITHUB_TOKEN; if (!repository) throw new Error("GITHUB_REPOSITORY is required for --upsert-issue"); @@ -143,7 +163,7 @@ function resolveRepoAndToken() { return { owner, repo, token }; } -async function findExistingIssue({ owner, repo, token }) { +async function findExistingIssue({ owner, repo, token }: { owner: string; repo: string; token: string }): Promise { let page = 1; while (page <= 10) { const issues = await githubRequest({ @@ -159,7 +179,7 @@ async function findExistingIssue({ owner, repo, token }) { return null; } -export function isReleaseWatchIssue(issue) { +export function isReleaseWatchIssue(issue: GitHubIssueCandidate): boolean { return ( !issue.pull_request && issue.user?.login === "github-actions[bot]" && @@ -170,7 +190,7 @@ export function isReleaseWatchIssue(issue) { ); } -async function githubRequest({ token, method, path, body }) { +async function githubRequest({ token, method, path, body }: { token: string; method: string; path: string; body?: unknown }): Promise { const response = await fetch(`https://api.github.com${path}`, { method, headers: { @@ -180,8 +200,8 @@ async function githubRequest({ token, method, path, body }) { "user-agent": "loopover-mcp-release-watch", "x-github-api-version": "2022-11-28", }, - body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS), + ...(body ? { body: JSON.stringify(body) } : {}), }); const text = await response.text(); const payload = text ? JSON.parse(text) : null; @@ -192,8 +212,7 @@ async function githubRequest({ token, method, path, body }) { return payload; } -const entrypointPath = process.argv[1] ? resolve(process.argv[1]) : ""; -if (import.meta.url === pathToFileURL(entrypointPath).href) { +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); diff --git a/scripts/check-miner-deployment-docs.d.mts b/scripts/check-miner-deployment-docs.d.mts deleted file mode 100644 index 40ac0e7f9a..0000000000 --- a/scripts/check-miner-deployment-docs.d.mts +++ /dev/null @@ -1,31 +0,0 @@ -export type MinerDeploymentReality = { - hasEnvRead: (name: string) => boolean; - /** Enumerable set of every real env-var read, for the reverse audit direction (#6601). */ - envReads: Iterable; - pathExists: (relativePath: string) => boolean; - isRegisteredCommand: (name: string) => boolean; -}; - -export type MinerDeploymentAuditResult = { - ok: boolean; - failures: string[]; - claimCounts: { - envVars: number; - filePaths: number; - subcommands: number; - }; -}; - -export function buildLiveMinerDeploymentReality(): MinerDeploymentReality; -export function runMinerDeploymentDocsAudit(opts?: { - testMode?: string | null; - reality?: MinerDeploymentReality; -}): MinerDeploymentAuditResult; - -export type MinerDeploymentAuditIo = { - log: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - exit: (code: number) => void; -}; - -export function main(env?: Record, io?: MinerDeploymentAuditIo): number; diff --git a/scripts/check-miner-deployment-docs.mjs b/scripts/check-miner-deployment-docs.ts similarity index 61% rename from scripts/check-miner-deployment-docs.mjs rename to scripts/check-miner-deployment-docs.ts index 018642d712..92a8af5715 100644 --- a/scripts/check-miner-deployment-docs.mjs +++ b/scripts/check-miner-deployment-docs.ts @@ -1,9 +1,9 @@ // CI entrypoint for the miner DEPLOYMENT.md accuracy audit (#6158). The pure checker lives in -// packages/loopover-miner/lib/deployment-docs-audit.js; this script builds live reality from the +// packages/loopover-miner/lib/deployment-docs-audit.ts; this script builds live reality from the // miner + engine trees and fails non-zero on drift so validate-code / test:ci catch renames. import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL, URL } from "node:url"; import { assertDeploymentDocsInSync, extractEnvVarClaims, @@ -11,6 +11,7 @@ import { extractSubcommandClaims, scanEnvVarTokens, scanRegisteredCommands, + type DeploymentDocsReality, } from "../packages/loopover-miner/lib/deployment-docs-audit.js"; const REPO_ROOT = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); @@ -21,37 +22,38 @@ const BIN_ENTRY = resolve(BIN_DIR, "loopover-miner.js"); const LIB_DIR = resolve(MINER_DIR, "lib"); const ENGINE_MINER_DIR = resolve(REPO_ROOT, "packages/loopover-engine/src/miner"); -function readFilesWithExtension(dir, extension) { +function readFilesWithExtension(dir: string, extension: string): string[] { return readdirSync(dir) .filter((name) => name.endsWith(extension)) .map((name) => readFileSync(join(dir, name), "utf8")); } /** Build the live reality predicates used by the audit (exported for unit tests). */ -export function buildLiveMinerDeploymentReality() { - const envReads = scanEnvVarTokens( - [ - ...readFilesWithExtension(LIB_DIR, ".js"), - ...readFilesWithExtension(BIN_DIR, ".js"), - ...readFilesWithExtension(ENGINE_MINER_DIR, ".ts"), - ].join("\n"), - ); +export function buildLiveMinerDeploymentReality(): DeploymentDocsReality { + const envReads = scanEnvVarTokens([...readFilesWithExtension(LIB_DIR, ".js"), ...readFilesWithExtension(BIN_DIR, ".js"), ...readFilesWithExtension(ENGINE_MINER_DIR, ".ts")].join("\n")); const registered = scanRegisteredCommands(readFileSync(BIN_ENTRY, "utf8")); return { - hasEnvRead: (name) => envReads.has(name), + hasEnvRead: (name: string) => envReads.has(name), // The full enumerable read-set (#6601), so auditDeploymentDocs can diff the reverse direction — a real // `LOOPOVER_MINER_*` read missing from DEPLOYMENT.md — not just probe one documented name at a time. envReads, - pathExists: (relativePath) => existsSync(resolve(MINER_DIR, relativePath)), - isRegisteredCommand: (name) => registered.has(name), + pathExists: (relativePath: string) => existsSync(resolve(MINER_DIR, relativePath)), + isRegisteredCommand: (name: string) => registered.has(name), }; } -/** - * Run the live DEPLOYMENT.md audit. - * @param {{ testMode?: string | null, reality?: ReturnType }} [opts] - */ -export function runMinerDeploymentDocsAudit(opts = {}) { +export type MinerDeploymentAuditResult = { + ok: boolean; + failures: string[]; + claimCounts: { + envVars: number; + filePaths: number; + subcommands: number; + }; +}; + +/** Run the live DEPLOYMENT.md audit. */ +export function runMinerDeploymentDocsAudit(opts: { testMode?: string | null; reality?: DeploymentDocsReality } = {}): MinerDeploymentAuditResult { const markdown = readFileSync(DEPLOYMENT_MD, "utf8"); const claims = { envVars: extractEnvVarClaims(markdown), @@ -78,18 +80,25 @@ export function runMinerDeploymentDocsAudit(opts = {}) { }; } -export function main(env = process.env, io = { - log: console.log.bind(console), - error: console.error.bind(console), - exit: (code) => process.exit(code), -}) { +export type MinerDeploymentAuditIo = { + log: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + exit: (code: number) => void; +}; + +export function main( + env: Record = process.env, + io: MinerDeploymentAuditIo = { + log: console.log.bind(console), + error: console.error.bind(console), + exit: (code: number) => process.exit(code), + }, +): number { try { const result = runMinerDeploymentDocsAudit({ testMode: env.CHECK_MINER_DEPLOYMENT_DOCS_AUDIT_TEST_MODE ?? null, }); - io.log( - `Miner deployment docs audit ok: ${result.claimCounts.envVars} env vars, ${result.claimCounts.filePaths} paths, ${result.claimCounts.subcommands} subcommands.`, - ); + io.log(`Miner deployment docs audit ok: ${result.claimCounts.envVars} env vars, ${result.claimCounts.filePaths} paths, ${result.claimCounts.subcommands} subcommands.`); return 0; } catch (error) { io.error(error instanceof Error ? error.message : String(error)); diff --git a/scripts/check-miner-package.mjs b/scripts/check-miner-package.mjs index 82e6e92e6c..28a4684047 100644 --- a/scripts/check-miner-package.mjs +++ b/scripts/check-miner-package.mjs @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs"; +import { FORBIDDEN_CONTENT } from "./forbidden-content.js"; const ALLOWED = [ /^bin\/loopover-miner\.(js|d\.ts)$/, diff --git a/scripts/check-node-version.d.mts b/scripts/check-node-version.d.mts deleted file mode 100644 index c836d2f3bb..0000000000 --- a/scripts/check-node-version.d.mts +++ /dev/null @@ -1,8 +0,0 @@ -export function checkNodeVersion(options?: { - nodeVersion?: string; - readFile?: () => string; -}): { - ok: boolean; - requiredRange: string | undefined; - nodeVersion?: string; -}; diff --git a/scripts/check-node-version.mjs b/scripts/check-node-version.ts similarity index 83% rename from scripts/check-node-version.mjs rename to scripts/check-node-version.ts index 661a592f7e..542f597e98 100644 --- a/scripts/check-node-version.mjs +++ b/scripts/check-node-version.ts @@ -18,13 +18,19 @@ // (incompletely) the only mechanism, missing 8 of 12 vitest-invoking script names (#7592-class gap, // caught by a repo-wide audit after #7619 shipped). import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, URL } from "node:url"; import semver from "semver"; -const PACKAGE_JSON_URL = new URL("../package.json", import.meta.url); +const PACKAGE_JSON_PATH = fileURLToPath(new URL("../package.json", import.meta.url)); -export function checkNodeVersion({ nodeVersion = process.version, readFile = () => readFileSync(PACKAGE_JSON_URL, "utf8") } = {}) { - const pkg = JSON.parse(readFile()); +export function checkNodeVersion({ + nodeVersion = process.version, + readFile = () => readFileSync(PACKAGE_JSON_PATH, "utf8"), +}: { + nodeVersion?: string; + readFile?: () => string; +} = {}): { ok: boolean; requiredRange: string | undefined; nodeVersion?: string } { + const pkg = JSON.parse(readFile()) as { engines?: { node?: string } }; const requiredRange = pkg.engines?.node; if (!requiredRange) return { ok: true, requiredRange: undefined }; diff --git a/scripts/check-openapi-settings-parity.d.mts b/scripts/check-openapi-settings-parity.d.mts deleted file mode 100644 index 12450bb099..0000000000 --- a/scripts/check-openapi-settings-parity.d.mts +++ /dev/null @@ -1,9 +0,0 @@ -export const TYPES_PATH: string; - -export const SETTINGS_PREVIEW_PATH: string; - -export function extractRepositorySettingsFieldNames(source: string): Set; - -export function extractRepoSettingsPreviewFieldNames(source: string): Set; - -export function diffFieldSets(typeFields: Set, schemaFields: Set): { missingFromSchema: string[]; extraInSchema: string[] }; diff --git a/scripts/check-openapi-settings-parity.mjs b/scripts/check-openapi-settings-parity.ts similarity index 93% rename from scripts/check-openapi-settings-parity.mjs rename to scripts/check-openapi-settings-parity.ts index 60e07ef284..4c60854e03 100644 --- a/scripts/check-openapi-settings-parity.mjs +++ b/scripts/check-openapi-settings-parity.ts @@ -7,7 +7,7 @@ // know about a field the spec doesn't mention. This is a structural key-set diff, not a value/type check. import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { RepositorySettingsSchema, RepoSettingsPreviewSchema } from "../src/openapi/schemas.ts"; +import { RepositorySettingsSchema, RepoSettingsPreviewSchema } from "../src/openapi/schemas.js"; export const TYPES_PATH = "src/types.ts"; const TYPE_START = "export type RepositorySettings = {"; @@ -24,15 +24,15 @@ const PREVIEW_SETTINGS_START = " settings: {"; /** Pure: extract the top-level field names of the `RepositorySettings` type from raw source text. Every * field is a primitive/union/type-alias reference (never an inline nested object literal), so this never * needs to track brace depth -- verified by direct inspection of the type at the time this check was added. */ -export function extractRepositorySettingsFieldNames(source) { +export function extractRepositorySettingsFieldNames(source: string): Set { const startIndex = source.indexOf(TYPE_START); if (startIndex === -1) throw new Error(`Could not find "${TYPE_START}" in the given source.`); const endIndex = source.indexOf("\n};", startIndex); if (endIndex === -1) throw new Error(`Could not find the closing "};" for RepositorySettings in the given source.`); const body = source.slice(startIndex + TYPE_START.length, endIndex); const fieldPattern = /^ {2}(\w+)\??:/gm; - const names = new Set(); - for (const match of body.matchAll(fieldPattern)) names.add(match[1]); + const names = new Set(); + for (const match of body.matchAll(fieldPattern)) names.add(match[1]!); return names; } @@ -40,7 +40,7 @@ export function extractRepositorySettingsFieldNames(source) { * source text. Unlike RepositorySettings, this block nests one inline object literal (`commandAuthorization`), * so we brace-match to bound the settings body precisely, then take only its direct children (4-space indent); * the nested members sit deeper and never match the anchor. */ -export function extractRepoSettingsPreviewFieldNames(source) { +export function extractRepoSettingsPreviewFieldNames(source: string): Set { const typeIndex = source.indexOf(PREVIEW_TYPE_START); if (typeIndex === -1) throw new Error(`Could not find "${PREVIEW_TYPE_START}" in the given source.`); const blockIndex = source.indexOf(PREVIEW_SETTINGS_START, typeIndex); @@ -58,13 +58,13 @@ export function extractRepoSettingsPreviewFieldNames(source) { if (endIndex === -1) throw new Error(`Could not find the closing "}" for the RepoSettingsPreview settings block in the given source.`); const body = source.slice(openBrace + 1, endIndex); const fieldPattern = /^ {4}(\w+)\??:/gm; - const names = new Set(); - for (const match of body.matchAll(fieldPattern)) names.add(match[1]); + const names = new Set(); + for (const match of body.matchAll(fieldPattern)) names.add(match[1]!); return names; } /** Pure: diff two field-name sets, returning the sorted asymmetric differences. */ -export function diffFieldSets(typeFields, schemaFields) { +export function diffFieldSets(typeFields: ReadonlySet, schemaFields: ReadonlySet): { missingFromSchema: string[]; extraInSchema: string[] } { return { missingFromSchema: [...typeFields].filter((field) => !schemaFields.has(field)).sort(), extraInSchema: [...schemaFields].filter((field) => !typeFields.has(field)).sort(), diff --git a/scripts/check-orb-release-due.mjs b/scripts/check-orb-release-due.mjs index 7340737a27..f84f3c785a 100644 --- a/scripts/check-orb-release-due.mjs +++ b/scripts/check-orb-release-due.mjs @@ -1,10 +1,10 @@ // Computes whether a new ORB (self-host container image) beta snapshot is due, and what its tag would be. // Read-only / side-effect-free by design: this script only REPORTS -- the actual `git tag` + `push` (the // consequential action) happens as explicit, auditable steps in .github/workflows/orb-beta-release.yml, not -// hidden inside this script. See scripts/orb-release-core.mjs for the underlying logic and rationale. +// hidden inside this script. See scripts/orb-release-core.ts for the underlying logic and rationale. import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; -import { buildOrbReleaseReport, latestOrbTag, latestStableOrbTag } from "./orb-release-core.mjs"; +import { buildOrbReleaseReport, latestOrbTag, latestStableOrbTag } from "./orb-release-core.js"; const MANIFEST_PATH = "orb-manifest.json"; diff --git a/scripts/check-orb-stable-release-due.mjs b/scripts/check-orb-stable-release-due.mjs index d8fe097382..be16c53129 100644 --- a/scripts/check-orb-stable-release-due.mjs +++ b/scripts/check-orb-stable-release-due.mjs @@ -2,10 +2,10 @@ // Read-only / side-effect-free by design, mirroring check-orb-release-due.mjs's own reasoning: the actual // orb-manifest.json bump + `git tag` + PR create/update (the consequential actions) happen as explicit, // auditable steps in .github/workflows/orb-stable-release-pr.yml, not hidden inside this script. See -// scripts/orb-release-core.mjs's buildOrbStableReleaseReport for the underlying logic and rationale. +// scripts/orb-release-core.ts's buildOrbStableReleaseReport for the underlying logic and rationale. import { execFileSync } from "node:child_process"; import { writeFileSync } from "node:fs"; -import { buildOrbStableReleaseReport } from "./orb-release-core.mjs"; +import { buildOrbStableReleaseReport } from "./orb-release-core.js"; function main() { const args = parseArgs(process.argv.slice(2)); diff --git a/scripts/check-schema-drift.d.mts b/scripts/check-schema-drift.d.mts deleted file mode 100644 index 9ed4c098d1..0000000000 --- a/scripts/check-schema-drift.d.mts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DatabaseSync } from "node:sqlite"; -import type { SQLiteTable } from "drizzle-orm/sqlite-core"; - -export const RAW_SQL_ONLY_TABLES: Set; - -export function replayMigrations(dir: string): DatabaseSync; - -export function listActualTables(db: DatabaseSync): Set; - -export function actualColumnsFor(db: DatabaseSync, table: string): Set; - -export function collectSchemaTables(schemaModule: Record): Map; - -export function declaredColumnsFor(table: SQLiteTable): Set; - -export function diffSchemaAgainstMigrations(db: DatabaseSync, schemaModule: Record, rawSqlOnlyTables?: Set): string[]; diff --git a/scripts/check-schema-drift.mjs b/scripts/check-schema-drift.ts old mode 100755 new mode 100644 similarity index 83% rename from scripts/check-schema-drift.mjs rename to scripts/check-schema-drift.ts index b700f7287e..c83685eaa5 --- a/scripts/check-schema-drift.mjs +++ b/scripts/check-schema-drift.ts @@ -16,15 +16,15 @@ // property name). Diff the two column-name sets per table. // // Run via `tsx` (not plain `node`) for the same reason as check-migrations.mjs and -// check-openapi-settings-parity.mjs: this script imports src/db/schema.ts (a .ts module) directly, and a bare +// check-openapi-settings-parity.ts: this script imports src/db/schema.ts (a .ts module) directly, and a bare // `node` invocation can't resolve a `.ts` import without an experimental flag CI's pinned Node isn't // guaranteed to support. import { readdirSync, readFileSync } from "node:fs"; import { DatabaseSync } from "node:sqlite"; import { fileURLToPath } from "node:url"; -import { getTableColumns, is } from "drizzle-orm"; +import { getTableColumns, getTableName, is } from "drizzle-orm"; import { SQLiteTable } from "drizzle-orm/sqlite-core"; -import * as schema from "../src/db/schema.ts"; +import * as schema from "../src/db/schema.js"; const MIGRATIONS_DIR = process.env.CHECK_SCHEMA_DRIFT_DIR || "migrations"; @@ -36,7 +36,7 @@ const MIGRATIONS_DIR = process.env.CHECK_SCHEMA_DRIFT_DIR || "migrations"; // it is a declared exception to "every migrated table must have a matching schema.ts declaration". Adding a // table here without also confirming it is genuinely raw-SQL-only is a reviewer-visible diff, not a silent // gap this check would otherwise catch. -export const RAW_SQL_ONLY_TABLES = new Set([ +export const RAW_SQL_ONLY_TABLES: Set = new Set([ "ams_instances", "ams_signals", "contributor_gate_history", @@ -65,13 +65,15 @@ export const RAW_SQL_ONLY_TABLES = new Set([ "tunables_overrides_shadow", ]); +type SqliteRow = Record; + /** Replay every migrations/*.sql file (concatenated, sorted -- the same order wrangler and * test/helpers/d1.ts's TestD1Database use) into a fresh in-memory SQLite DB and return it. Deliberately not * importing TestD1Database directly: that helper caches the concatenated SQL into module-scope state sized * for the test suite's lifetime and always reads the repo's real `migrations/` dir, whereas this script's * tests need an independently directed migrations dir (CHECK_SCHEMA_DRIFT_DIR) to exercise a deliberately * drifted fixture without touching the real migrations/. */ -export function replayMigrations(dir) { +export function replayMigrations(dir: string): DatabaseSync { const sql = readdirSync(dir) .filter((file) => file.endsWith(".sql")) .sort() @@ -83,33 +85,36 @@ export function replayMigrations(dir) { } /** The set of real, non-sqlite-internal table names present in a replayed migrations DB. */ -export function listActualTables(db) { - const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all(); +export function listActualTables(db: DatabaseSync): Set { + const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all() as SqliteRow[]; return new Set(rows.map((row) => String(row.name))); } /** The actual DB column names (via PRAGMA table_info's `name`, not cid/type/etc.) for one table. */ -export function actualColumnsFor(db, table) { - const rows = db.prepare(`PRAGMA table_info(${table})`).all(); +export function actualColumnsFor(db: DatabaseSync, table: string): Set { + const rows = db.prepare(`PRAGMA table_info(${table})`).all() as SqliteRow[]; return new Set(rows.map((row) => String(row.name))); } /** Every exported drizzle sqliteTable in the given schema module, keyed by its declared DB table name (not * the JS export name -- schema.ts's export identifiers are camelCase, but getTableColumns / PRAGMA both key * on the actual snake_case DB name). Guards against a non-table export (schema.ts today exports only - * tables, but this stays correct if a helper/type export is ever added alongside them). */ -export function collectSchemaTables(schemaModule) { - const tables = new Map(); + * tables, but this stays correct if a helper/type export is ever added alongside them). Uses drizzle-orm's + * public getTableName() rather than the SQLiteTable.Symbol.Name internal (the original untyped .mjs used + * that internal directly; it's marked `@internal` in drizzle-orm's own source and isn't part of its public + * type exports, which is exactly what surfaced here once this file gained real types to check against). */ +export function collectSchemaTables(schemaModule: Record): Map { + const tables = new Map(); for (const value of Object.values(schemaModule)) { if (!is(value, SQLiteTable)) continue; - tables.set(value[SQLiteTable.Symbol.Name], value); + tables.set(getTableName(value), value); } return tables; } /** The declared DB column names (getTableColumns(table)[key].name -- the actual DB column, not the JS * property name) for one drizzle table object. */ -export function declaredColumnsFor(table) { +export function declaredColumnsFor(table: SQLiteTable): Set { return new Set(Object.values(getTableColumns(table)).map((column) => column.name)); } @@ -119,10 +124,10 @@ export function declaredColumnsFor(table) { * effects of its own, so it's directly unit-testable against a hand-built fixture DB + fake schema module. * Returns one mismatch entry per (table, column) or per whole missing table; empty when they agree. */ -export function diffSchemaAgainstMigrations(db, schemaModule, rawSqlOnlyTables = RAW_SQL_ONLY_TABLES) { +export function diffSchemaAgainstMigrations(db: DatabaseSync, schemaModule: Record, rawSqlOnlyTables: ReadonlySet = RAW_SQL_ONLY_TABLES): string[] { const actualTables = listActualTables(db); const schemaTables = collectSchemaTables(schemaModule); - const mismatches = []; + const mismatches: string[] = []; for (const [tableName, table] of [...schemaTables].sort(([a], [b]) => a.localeCompare(b))) { if (!actualTables.has(tableName)) { @@ -145,7 +150,7 @@ export function diffSchemaAgainstMigrations(db, schemaModule, rawSqlOnlyTables = // feature table (rawSqlOnlyTables) or an undeclared drift -- flag anything not on the allowlist. for (const tableName of [...actualTables].sort()) { if (schemaTables.has(tableName) || rawSqlOnlyTables.has(tableName)) continue; - mismatches.push(`table "${tableName}" exists in migrations/ but has no src/db/schema.ts declaration and is not in RAW_SQL_ONLY_TABLES (scripts/check-schema-drift.mjs)`); + mismatches.push(`table "${tableName}" exists in migrations/ but has no src/db/schema.ts declaration and is not in RAW_SQL_ONLY_TABLES (scripts/check-schema-drift.ts)`); } return mismatches; @@ -158,7 +163,7 @@ function main() { if (mismatches.length > 0) { process.stderr.write(`check-schema-drift: src/db/schema.ts has drifted from migrations/ -- ${mismatches.length} mismatch(es):\n`); for (const mismatch of mismatches) process.stderr.write(` - ${mismatch}\n`); - process.stderr.write("Fix by either updating src/db/schema.ts to match migrations/, adding a migration for the missing column/table, or (for an intentionally raw-SQL-only table) adding it to RAW_SQL_ONLY_TABLES in scripts/check-schema-drift.mjs.\n"); + process.stderr.write("Fix by either updating src/db/schema.ts to match migrations/, adding a migration for the missing column/table, or (for an intentionally raw-SQL-only table) adding it to RAW_SQL_ONLY_TABLES in scripts/check-schema-drift.ts.\n"); process.exit(1); } diff --git a/scripts/check-stuck-required-checks.d.mts b/scripts/check-stuck-required-checks.d.mts deleted file mode 100644 index af99b5b3e8..0000000000 --- a/scripts/check-stuck-required-checks.d.mts +++ /dev/null @@ -1,53 +0,0 @@ -export type GithubApi = ( - path: string, - options?: { method?: string; body?: string; headers?: Record }, -) => Promise; - -export type PullRequestRef = { - number: number; - draft?: boolean; - head: { sha: string }; -}; - -export type StuckCheck = { - name: string; - status: string; - startedAt: string; - elapsedMinutes: number; - htmlUrl?: string; -}; - -export type CommentScope = { - githubApi: GithubApi; - owner: string; - repoName: string; -}; - -export type FindStuckOptions = CommentScope & { thresholdMinutes: number }; - -export declare const MARKER: string; -export declare const REQUIRED_CONTEXTS: Set; -export declare const EXTERNAL_REQUIRED_CHECKS: Set; - -export declare function minutesSince(isoString: string): number; - -export declare function findStuckChecksForPr( - pr: PullRequestRef, - requiredContexts: Set, - options: FindStuckOptions, -): Promise; - -export declare function hasExistingWatchdogComment( - prNumber: number, - options: CommentScope, -): Promise; - -export declare function runStuckCheckWatchdog(options: { - githubApi: GithubApi; - owner: string; - repoName: string; - thresholdMinutes: number; - dryRun?: boolean; - requiredContexts?: Set; - log?: (message: string) => void; -}): Promise; diff --git a/scripts/check-stuck-required-checks.mjs b/scripts/check-stuck-required-checks.ts similarity index 78% rename from scripts/check-stuck-required-checks.mjs rename to scripts/check-stuck-required-checks.ts index 5f1c585dd7..5c230dba25 100644 --- a/scripts/check-stuck-required-checks.mjs +++ b/scripts/check-stuck-required-checks.ts @@ -32,10 +32,17 @@ export const REQUIRED_CONTEXTS = new Set(["validate", "Superagent Security Scan" // is documented next to the hardcoded list rather than hidden in a test (#7774). export const EXTERNAL_REQUIRED_CHECKS = new Set(["Superagent Security Scan"]); +// Deliberately `any`, not `unknown`: every caller below accesses response-shaped properties +// (checkRuns.check_runs, comments.some(...), prs.filter(...)) directly off the resolved value with no +// narrowing step, matching how this always actually ran as untyped JS -- typing this `unknown` would +// just force a wall of `as` casts at every call site without adding any real safety, since the GitHub +// API's actual response shape isn't validated here either way. +export type GithubApi = (path: string, options?: { method?: string; body?: string; headers?: Record }) => Promise; + /** Build the authenticated GitHub API caller used by the live entrypoint. Kept as a factory so the * pure/testable logic below can take `githubApi` as an injected dependency instead of closing over a * module-level token. */ -function makeGithubApi(token) { +function makeGithubApi(token: string): GithubApi { return async function githubApi(path, options = {}) { const response = await fetch(`https://api.github.com${path}`, { ...options, @@ -53,13 +60,37 @@ function makeGithubApi(token) { }; } -export function minutesSince(isoString) { +export function minutesSince(isoString: string): number { return (Date.now() - new Date(isoString).getTime()) / 60000; } -export async function findStuckChecksForPr(pr, requiredContexts, { githubApi, owner, repoName, thresholdMinutes }) { +export type PullRequestRef = { + number: number; + draft?: boolean; + head: { sha: string }; +}; + +export type StuckCheck = { + name: string; + status: string; + startedAt: string; + elapsedMinutes: number; + htmlUrl?: string; +}; + +export type CommentScope = { + githubApi: GithubApi; + owner: string; + repoName: string; +}; + +export async function findStuckChecksForPr( + pr: PullRequestRef, + requiredContexts: Set, + { githubApi, owner, repoName, thresholdMinutes }: CommentScope & { thresholdMinutes: number }, +): Promise { const checkRuns = await githubApi(`/repos/${owner}/${repoName}/commits/${pr.head.sha}/check-runs?per_page=100`); - const stuck = []; + const stuck: StuckCheck[] = []; for (const run of checkRuns.check_runs ?? []) { if (!requiredContexts.has(run.name)) continue; if (run.status === "completed") continue; @@ -71,12 +102,12 @@ export async function findStuckChecksForPr(pr, requiredContexts, { githubApi, ow return stuck; } -export async function hasExistingWatchdogComment(prNumber, { githubApi, owner, repoName }) { +export async function hasExistingWatchdogComment(prNumber: number, { githubApi, owner, repoName }: CommentScope): Promise { const comments = await githubApi(`/repos/${owner}/${repoName}/issues/${prNumber}/comments?per_page=100`); - return comments.some((comment) => comment.body?.includes(MARKER)); + return comments.some((comment: { body?: string }) => comment.body?.includes(MARKER)); } -async function postComment(prNumber, stuckChecks, { githubApi, owner, repoName }) { +async function postComment(prNumber: number, stuckChecks: StuckCheck[], { githubApi, owner, repoName }: CommentScope) { const lines = [ MARKER, "## ⚠️ A required check looks stuck", @@ -106,8 +137,16 @@ export async function runStuckCheckWatchdog({ dryRun = false, requiredContexts = REQUIRED_CONTEXTS, log = console.log, -}) { - const prs = (await githubApi(`/repos/${owner}/${repoName}/pulls?state=open&per_page=100`)).filter((pr) => !pr.draft); +}: { + githubApi: GithubApi; + owner: string; + repoName: string; + thresholdMinutes: number; + dryRun?: boolean; + requiredContexts?: Set; + log?: (message: string) => void; +}): Promise { + const prs = (await githubApi(`/repos/${owner}/${repoName}/pulls?state=open&per_page=100`)).filter((pr: PullRequestRef) => !pr.draft); let flaggedCount = 0; for (const pr of prs) { @@ -147,5 +186,5 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { const token = process.env.GITHUB_TOKEN; if (!token) throw new Error("GITHUB_TOKEN is required"); - await runStuckCheckWatchdog({ githubApi: makeGithubApi(token), owner, repoName, thresholdMinutes, dryRun }); + await runStuckCheckWatchdog({ githubApi: makeGithubApi(token), owner: owner!, repoName: repoName!, thresholdMinutes, dryRun }); } diff --git a/scripts/check-ui-mcp-version-copy.d.mts b/scripts/check-ui-mcp-version-copy.d.mts deleted file mode 100644 index f11420d1e6..0000000000 --- a/scripts/check-ui-mcp-version-copy.d.mts +++ /dev/null @@ -1,37 +0,0 @@ -export declare const SOURCE_LATEST_PATH: string; -export declare const SCAN_TARGETS: string[]; - -export type StaleVersionMatchers = { - floorVersion: string; - minorLabel: string; - visibleVersion: RegExp; - versionRange: RegExp; - floor: RegExp; -}; - -export declare function collectVersionCopyFailures(input: { - label: string; - text: string; - matchers: StaleVersionMatchers; -}): string[]; - -export declare function collectSourceFiles(path: string): string[]; - -export declare function isTextSource(path: string): boolean; - -export declare function isMinimumSupportedContext(line: string): boolean; - -export declare function buildStaleVersionMatchers( - floorVersion: string, -): StaleVersionMatchers; - -export declare function readKnownLatestVersion(path: string): string; - -export declare function writeKnownLatestVersion( - path: string, - newVersion: string, -): void; - -export declare function readMinimumSupportedVersion(path: string): string; - -export declare function fetchLatestVersion(): Promise; diff --git a/scripts/check-ui-mcp-version-copy.mjs b/scripts/check-ui-mcp-version-copy.ts similarity index 60% rename from scripts/check-ui-mcp-version-copy.mjs rename to scripts/check-ui-mcp-version-copy.ts index 7e368acfdb..72e5fc6edb 100644 --- a/scripts/check-ui-mcp-version-copy.mjs +++ b/scripts/check-ui-mcp-version-copy.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; -import { join, relative } from "node:path"; import { get } from "node:https"; +import { join, relative } from "node:path"; import { pathToFileURL } from "node:url"; const packageName = "@loopover/mcp"; @@ -22,6 +22,14 @@ export const SCAN_TARGETS = [ "apps/loopover-ui/content", ]; +export type StaleVersionMatchers = { + floorVersion: string; + minorLabel: string; + visibleVersion: RegExp; + versionRange: RegExp; + floor: RegExp; +}; + async function main() { const root = process.cwd(); const sourceLatestPath = join(root, SOURCE_LATEST_PATH); @@ -36,15 +44,12 @@ async function main() { try { latest = await fetchLatestVersion(); } catch (error) { - latestSkipReason = - error instanceof Error ? error.message : "unknown error"; + latestSkipReason = error instanceof Error ? error.message : "unknown error"; } } const sourceLatest = readKnownLatestVersion(sourceLatestPath); - const matchers = buildStaleVersionMatchers( - readMinimumSupportedVersion(sourceLatestPath), - ); - const failures = []; + const matchers = buildStaleVersionMatchers(readMinimumSupportedVersion(sourceLatestPath)); + const failures: string[] = []; // --write: self-heal the known-latest fallback instead of failing, so keeping it in sync with npm is a // scheduled bot commit (see .github/workflows/mcp-ui-version-sync.yml), never a manual edit (#6580). CI's @@ -53,13 +58,9 @@ async function main() { if (latest && sourceLatest !== latest) { if (write) { writeKnownLatestVersion(sourceLatestPath, latest); - console.log( - `${SOURCE_LATEST_PATH}: updated known latest ${sourceLatest} -> ${latest}`, - ); + console.log(`${SOURCE_LATEST_PATH}: updated known latest ${sourceLatest} -> ${latest}`); } else { - failures.push( - `${SOURCE_LATEST_PATH}: known latest ${sourceLatest} does not match npm dist-tags.latest ${latest}`, - ); + failures.push(`${SOURCE_LATEST_PATH}: known latest ${sourceLatest} does not match npm dist-tags.latest ${latest}`); } } else if (!latest) { console.warn( @@ -78,100 +79,72 @@ async function main() { process.exit(1); } - console.log( - `MCP UI version copy ok: npm latest ${latest ?? "unchecked"}, minimum floor ${matchers.floorVersion}, scanned ${targets.length} target(s)`, - ); + console.log(`MCP UI version copy ok: npm latest ${latest ?? "unchecked"}, minimum floor ${matchers.floorVersion}, scanned ${targets.length} target(s)`); } -export function collectVersionCopyFailures({ label, text, matchers }) { - const failures = []; +export function collectVersionCopyFailures({ label, text, matchers }: { label: string; text: string; matchers: StaleVersionMatchers }): string[] { + const failures: string[] = []; const lines = text.split(/\r?\n/); lines.forEach((line, index) => { const lineNumber = index + 1; if (matchers.visibleVersion.test(line)) { - failures.push( - `${label}:${lineNumber}: stale visible v${matchers.minorLabel} version text`, - ); + failures.push(`${label}:${lineNumber}: stale visible v${matchers.minorLabel} version text`); } if (matchers.versionRange.test(line)) { - failures.push( - `${label}:${lineNumber}: stale ${matchers.minorLabel}.x package-version range`, - ); + failures.push(`${label}:${lineNumber}: stale ${matchers.minorLabel}.x package-version range`); } if (matchers.floor.test(line) && !isMinimumSupportedContext(line)) { - failures.push( - `${label}:${lineNumber}: ${matchers.floorVersion} is only allowed as an explicit minimum-supported compatibility floor`, - ); + failures.push(`${label}:${lineNumber}: ${matchers.floorVersion} is only allowed as an explicit minimum-supported compatibility floor`); } if (/@loopover\/mcp(?:\s+|@)v?\d+\.\d+\.\d+/.test(line)) { - failures.push( - `${label}:${lineNumber}: hardcoded ${packageName} display version`, - ); + failures.push(`${label}:${lineNumber}: hardcoded ${packageName} display version`); } if (/(?:npm (?:i|install) -g|npx -y)\s+@loopover\/mcp(?!@)/.test(line)) { - failures.push( - `${label}:${lineNumber}: install command must use ${packageName}@latest or resolved npm latest`, - ); + failures.push(`${label}:${lineNumber}: install command must use ${packageName}@latest or resolved npm latest`); } - if ( - /args\s*=\s*\[.*"@loopover\/mcp"/.test(line) || - /"args":\s*\[.*"@loopover\/mcp"/.test(line) - ) { - failures.push( - `${label}:${lineNumber}: MCP client args must use ${packageName}@latest or resolved npm latest`, - ); + if (/args\s*=\s*\[.*"@loopover\/mcp"/.test(line) || /"args":\s*\[.*"@loopover\/mcp"/.test(line)) { + failures.push(`${label}:${lineNumber}: MCP client args must use ${packageName}@latest or resolved npm latest`); } }); return failures; } -export function collectSourceFiles(path) { +export function collectSourceFiles(path: string): string[] { const stat = statSync(path); if (stat.isFile()) return isTextSource(path) ? [path] : []; return readdirSync(path).flatMap((entry) => { const next = join(path, entry); - if ( - entry === "node_modules" || - entry === "dist" || - entry === ".vitepress" || - entry === "coverage" - ) - return []; - if (/routeTree\.gen\.ts$/.test(next) || /public\/openapi\.json$/.test(next)) - return []; + if (entry === "node_modules" || entry === "dist" || entry === ".vitepress" || entry === "coverage") return []; + if (/routeTree\.gen\.ts$/.test(next) || /public\/openapi\.json$/.test(next)) return []; return collectSourceFiles(next); }); } -export function isTextSource(path) { +export function isTextSource(path: string): boolean { return /\.(md|mdx|ts|tsx|js|jsx|json)$/.test(path); } -export function isMinimumSupportedContext(line) { - return /minimum[_ -]?supported|MCP_MINIMUM_SUPPORTED_VERSION|MINIMUM_SUPPORTED_MCP_VERSION|compatibility floor|API minimum|supportedVersionRange/i.test( - line, - ); +export function isMinimumSupportedContext(line: string): boolean { + return /minimum[_ -]?supported|MCP_MINIMUM_SUPPORTED_VERSION|MINIMUM_SUPPORTED_MCP_VERSION|compatibility floor|API minimum|supportedVersionRange/i.test(line); } -function escapeRegExp(value) { +function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // Derives the three stale-version matchers from the current minimum-supported floor. Keeping the shapes in // one place (visible `vX.Y`, the `X.Y.x` range, and the bare `X.Y.Z` floor allowed only in an explicit // minimum-supported statement) means the deterministic scan tracks the floor instead of a frozen literal. -export function buildStaleVersionMatchers(floorVersion) { +export function buildStaleVersionMatchers(floorVersion: string): StaleVersionMatchers { if (!/^\d+\.\d+\.\d+$/.test(floorVersion)) { - throw new Error( - `Expected a semver minimum-supported floor like 0.5.0, got "${floorVersion}".`, - ); + throw new Error(`Expected a semver minimum-supported floor like 0.5.0, got "${floorVersion}".`); } const [major, minor, patch] = floorVersion.split("."); const minorLabel = `${major}.${minor}`; const minorPattern = escapeRegExp(minorLabel); - const patchPattern = escapeRegExp(patch); + const patchPattern = escapeRegExp(patch!); const floorPattern = escapeRegExp(floorVersion); return { floorVersion, @@ -182,74 +155,53 @@ export function buildStaleVersionMatchers(floorVersion) { }; } -export function readKnownLatestVersion(path) { +export function readKnownLatestVersion(path: string): string { const text = readFileSync(path, "utf8"); const match = /MCP_PACKAGE_KNOWN_LATEST_VERSION\s*=\s*"([^"]+)"/.exec(text); - if (!match) - throw new Error("Could not find MCP_PACKAGE_KNOWN_LATEST_VERSION."); - return match[1]; + if (!match) throw new Error("Could not find MCP_PACKAGE_KNOWN_LATEST_VERSION."); + return match[1]!; } -export function writeKnownLatestVersion(path, newVersion) { +export function writeKnownLatestVersion(path: string, newVersion: string): void { const text = readFileSync(path, "utf8"); const pattern = /MCP_PACKAGE_KNOWN_LATEST_VERSION\s*=\s*"([^"]+)"/; - if (!pattern.test(text)) - throw new Error("Could not find MCP_PACKAGE_KNOWN_LATEST_VERSION."); - const updated = text.replace( - pattern, - `MCP_PACKAGE_KNOWN_LATEST_VERSION = "${newVersion}"`, - ); + if (!pattern.test(text)) throw new Error("Could not find MCP_PACKAGE_KNOWN_LATEST_VERSION."); + const updated = text.replace(pattern, `MCP_PACKAGE_KNOWN_LATEST_VERSION = "${newVersion}"`); writeFileSync(path, updated); } -export function readMinimumSupportedVersion(path) { +export function readMinimumSupportedVersion(path: string): string { const text = readFileSync(path, "utf8"); const match = /MCP_MINIMUM_SUPPORTED_VERSION\s*=\s*"([^"]+)"/.exec(text); if (!match) throw new Error("Could not find MCP_MINIMUM_SUPPORTED_VERSION."); - return match[1]; + return match[1]!; } -export function fetchLatestVersion() { +export function fetchLatestVersion(): Promise { return new Promise((resolve, reject) => { - const request = get( - registryUrl, - { headers: { accept: "application/json" } }, - (response) => { - let body = ""; - response.setEncoding("utf8"); - response.on("data", (chunk) => { - body += chunk; - }); - response.on("end", () => { - if ( - !response.statusCode || - response.statusCode < 200 || - response.statusCode >= 300 - ) { - reject( - new Error( - `npm registry returned ${response.statusCode ?? "unknown"}`, - ), - ); + const request = get(registryUrl, { headers: { accept: "application/json" } }, (response) => { + let body = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { + body += chunk; + }); + response.on("end", () => { + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`npm registry returned ${response.statusCode ?? "unknown"}`)); + return; + } + try { + const latest = JSON.parse(body)?.["dist-tags"]?.latest; + if (typeof latest !== "string" || !/^\d+\.\d+\.\d+$/.test(latest)) { + reject(new Error("npm registry did not return a stable latest version")); return; } - try { - const latest = JSON.parse(body)?.["dist-tags"]?.latest; - if (typeof latest !== "string" || !/^\d+\.\d+\.\d+$/.test(latest)) { - reject( - new Error( - "npm registry did not return a stable latest version", - ), - ); - return; - } - resolve(latest); - } catch (error) { - reject(error); - } - }); - }, - ); + resolve(latest); + } catch (error) { + reject(error); + } + }); + }); request.setTimeout(8000, () => { request.destroy(new Error("npm registry timeout")); }); diff --git a/scripts/ci-duration-report.d.mts b/scripts/ci-duration-report.d.mts deleted file mode 100644 index f2f2c05361..0000000000 --- a/scripts/ci-duration-report.d.mts +++ /dev/null @@ -1,18 +0,0 @@ -export type WorkflowRun = { - conclusion: string; - created_at: string; - updated_at: string; -}; - -export type CiDurationSummary = { - count: number; - excludedCancelled: number; - p50Seconds: number | null; - p95Seconds: number | null; - failureRate: number | null; - failures: number; -}; - -export declare function durationSeconds(run: WorkflowRun): number; -export declare function percentile(sortedValues: number[], p: number): number | null; -export declare function summarize(allRuns: WorkflowRun[]): CiDurationSummary; diff --git a/scripts/ci-duration-report.mjs b/scripts/ci-duration-report.ts similarity index 78% rename from scripts/ci-duration-report.mjs rename to scripts/ci-duration-report.ts index 1d06cc23a7..2a612e3d79 100644 --- a/scripts/ci-duration-report.mjs +++ b/scripts/ci-duration-report.ts @@ -9,8 +9,28 @@ import { writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; -async function fetchAllRuns(repo, token, sinceIso) { - const runs = []; +// The shape durationSeconds/percentile/summarize actually consume -- deliberately just these three +// fields, not the full GitHub API run object, so a test fixture never has to fabricate irrelevant +// properties. `event` lives on RawWorkflowRun below since only main()'s trigger split reads it. +export type WorkflowRun = { + conclusion: string; + created_at: string; + updated_at: string; +}; + +type RawWorkflowRun = WorkflowRun & { event: string }; + +export type CiDurationSummary = { + count: number; + excludedCancelled: number; + p50Seconds: number | null; + p95Seconds: number | null; + failureRate: number | null; + failures: number; +}; + +async function fetchAllRuns(repo: string, token: string, sinceIso: string): Promise { + const runs: RawWorkflowRun[] = []; let page = 1; for (;;) { const response = await fetch( @@ -26,7 +46,7 @@ async function fetchAllRuns(repo, token, sinceIso) { if (!response.ok) { throw new Error(`GitHub API error ${response.status}: ${await response.text()}`); } - const body = await response.json(); + const body = (await response.json()) as { workflow_runs: RawWorkflowRun[] }; runs.push(...body.workflow_runs); if (body.workflow_runs.length < 100) break; page += 1; @@ -35,17 +55,17 @@ async function fetchAllRuns(repo, token, sinceIso) { return runs; } -export function durationSeconds(run) { +export function durationSeconds(run: WorkflowRun): number { return (new Date(run.updated_at).getTime() - new Date(run.created_at).getTime()) / 1000; } -export function percentile(sortedValues, p) { +export function percentile(sortedValues: number[], p: number): number | null { if (sortedValues.length === 0) return null; const index = Math.min(sortedValues.length - 1, Math.ceil((p / 100) * sortedValues.length) - 1); - return sortedValues[Math.max(0, index)]; + return sortedValues[Math.max(0, index)]!; } -export function summarize(allRuns) { +export function summarize(allRuns: WorkflowRun[]): CiDurationSummary { // "cancelled" excluded entirely, not just from the failure count: this workflow's own // cancel-in-progress concurrency setting means a cancelled run is almost always a rapid re-push // superseding its predecessor mid-run, not CI breaking -- counting it as a failure (or even as a diff --git a/scripts/deploy-selfhost-prebuilt.sh b/scripts/deploy-selfhost-prebuilt.sh index 7dab684d26..451de04cbf 100755 --- a/scripts/deploy-selfhost-prebuilt.sh +++ b/scripts/deploy-selfhost-prebuilt.sh @@ -36,7 +36,7 @@ run_node_build() { -v "$PWD:/work" \ -w /work \ "$NODE_IMAGE" \ - sh -lc 'npm ci --ignore-scripts && npm --workspace @loopover/engine run build && node scripts/build-selfhost.mjs --all && node scripts/validate-selfhost-sourcemap.mjs' + sh -lc 'npm ci --ignore-scripts && npm --workspace @loopover/engine run build && node scripts/build-selfhost.mjs --all && node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts' } run_sentry_upload() { @@ -74,7 +74,7 @@ run_sentry_upload() { -v "$PWD:/work" \ -w /work \ "$NODE_IMAGE" \ - sh -lc 'apt-get update >/dev/null && apt-get install -y --no-install-recommends ca-certificates git >/dev/null && git config --global --add safe.directory /work && (npx -y "$SENTRY_CLI_PACKAGE" releases new "$SENTRY_RELEASE" >/tmp/loopover-sentry-release-new.log 2>&1 || true) && npx -y "$SENTRY_CLI_PACKAGE" releases set-commits "$SENTRY_RELEASE" --auto && npx -y "$SENTRY_CLI_PACKAGE" sourcemaps inject dist && node scripts/validate-selfhost-sourcemap.mjs && npx -y "$SENTRY_CLI_PACKAGE" sourcemaps upload --release="$SENTRY_RELEASE" dist && npx -y "$SENTRY_CLI_PACKAGE" releases finalize "$SENTRY_RELEASE" && chown -R "$HOST_UID:$HOST_GID" dist node_modules package-lock.json' + sh -lc 'apt-get update >/dev/null && apt-get install -y --no-install-recommends ca-certificates git >/dev/null && git config --global --add safe.directory /work && (npx -y "$SENTRY_CLI_PACKAGE" releases new "$SENTRY_RELEASE" >/tmp/loopover-sentry-release-new.log 2>&1 || true) && npx -y "$SENTRY_CLI_PACKAGE" releases set-commits "$SENTRY_RELEASE" --auto && npx -y "$SENTRY_CLI_PACKAGE" sourcemaps inject dist && node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts && npx -y "$SENTRY_CLI_PACKAGE" sourcemaps upload --release="$SENTRY_RELEASE" dist && npx -y "$SENTRY_CLI_PACKAGE" releases finalize "$SENTRY_RELEASE" && chown -R "$HOST_UID:$HOST_GID" dist node_modules package-lock.json' } run_init_secrets() { diff --git a/scripts/export-d1-core.d.mts b/scripts/export-d1-core.d.mts deleted file mode 100644 index 631427945b..0000000000 --- a/scripts/export-d1-core.d.mts +++ /dev/null @@ -1,26 +0,0 @@ -export const EXCLUDED_TABLES: Set; -export const REDACTED_COLUMNS: Record; - -export type D1Row = Record; - -export type TableExport = { - table: string; - rowCount: number; - redactedColumns: string[]; - checksum: string; - rows: D1Row[]; -}; - -export type ExportManifest = { - tableCount: number; - totalRows: number; - tables: Array<{ table: string; rowCount: number; redactedColumns: string[]; checksum: string }>; - [meta: string]: unknown; -}; - -export function isSafeTableName(name: unknown): boolean; -export function redactRow(table: string, row: D1Row): D1Row; -export function checksumRows(rows: D1Row[]): string; -export function filterRowsSince(rows: D1Row[], sinceColumn: string | undefined, sinceDate: string | undefined): D1Row[]; -export function buildTableExport(table: string, rows: D1Row[], opts?: { sinceColumn?: string; sinceDate?: string }): TableExport | null; -export function buildExportManifest(tableExports: Array, meta?: Record): ExportManifest; diff --git a/scripts/export-d1-core.mjs b/scripts/export-d1-core.ts similarity index 81% rename from scripts/export-d1-core.mjs rename to scripts/export-d1-core.ts index 415e8fc2e6..3b685734f2 100644 --- a/scripts/export-d1-core.mjs +++ b/scripts/export-d1-core.ts @@ -1,16 +1,33 @@ // Pure core for the self-host D1 export (#selfhost-migration Phase 3). Read-only: it transforms rows already // SELECTed from the cloud D1 into a redacted, checksummed, category-organized export the self-host importer can -// load idempotently. No IO here — the CLI (export-d1-data.mjs) does the wrangler/D1 reads and the file writes — +// load idempotently. No IO here — the CLI (export-d1-data.ts) does the wrangler/D1 reads and the file writes — // so this stays unit-testable. See [[gittensory-selfhost-migration-plan]]. import { createHash } from "node:crypto"; +export type D1Row = Record; + +export type TableExport = { + table: string; + rowCount: number; + redactedColumns: string[]; + checksum: string; + rows: D1Row[]; +}; + +export type ExportManifest = { + tableCount: number; + totalRows: number; + tables: Array<{ table: string; rowCount: number; redactedColumns: string[]; checksum: string }>; + [meta: string]: unknown; +}; + // Tables NEVER exported: SQLite/Drizzle internals, the cloud's own migration ledger (the self-host applies its // own forward migrations), plus private cloud-only calibration signals that must not cross instance boundaries. // predicted_gate_calls (0137) and predicted_gate_calibration_ledger (0138) are both login-keyed, LOCAL-ONLY by // their own migration docstrings ("never wired into exportOrbBatch or any other cross-instance/public export // path") — same sensitivity class, same exclusion. // Keep this conservative — excluding a real table loses data; the importer also skips it. -export const EXCLUDED_TABLES = new Set([ +export const EXCLUDED_TABLES: Set = new Set([ "sqlite_sequence", "sqlite_stat1", "d1_migrations", @@ -32,7 +49,7 @@ export const EXCLUDED_TABLES = new Set([ // • orb_enrollments.secret_hash — one-time enrollment secret hashes. // • orb_enrollments.relay_secret_* — encrypted relay webhook signing secret material. // • orb_enrollments.cached_token_json — encrypted GitHub installation-token cache envelope. -export const REDACTED_COLUMNS = { +export const REDACTED_COLUMNS: Record = { auth_sessions: ["token_hash"], webhook_events: ["payload_hash"], repository_ai_keys: ["ciphertext"], @@ -48,15 +65,15 @@ export const REDACTED_COLUMNS = { const SAFE_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/; /** True when `name` is a plain SQL identifier safe to interpolate into a quoted-table SELECT. */ -export function isSafeTableName(name) { +export function isSafeTableName(name: unknown): boolean { return typeof name === "string" && SAFE_TABLE_NAME.test(name); } /** Drop the redacted columns for `table` from a single row (returns the row unchanged when nothing is redacted). */ -export function redactRow(table, row) { +export function redactRow(table: string, row: D1Row): D1Row { const drop = REDACTED_COLUMNS[table]; if (!drop || drop.length === 0) return row; - const out = {}; + const out: D1Row = {}; for (const [key, value] of Object.entries(row)) { if (!drop.includes(key)) out[key] = value; } @@ -64,12 +81,12 @@ export function redactRow(table, row) { } /** Canonicalize a row (sort keys) so column-order differences between D1/SQLite/Postgres don't change the checksum. */ -function canonicalizeRow(row) { +function canonicalizeRow(row: D1Row): D1Row { return Object.fromEntries(Object.entries(row).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))); } /** Deterministic SHA-256 over the canonicalized rows, so a self-host import can prove a faithful round-trip. */ -export function checksumRows(rows) { +export function checksumRows(rows: D1Row[]): string { return createHash("sha256").update(JSON.stringify(rows.map(canonicalizeRow))).digest("hex"); } @@ -79,7 +96,7 @@ export function checksumRows(rows) { * missing the column (or with a non-string value) is KEPT — fail-safe: an incremental pass never silently drops a * row it can't time-compare (the importer's idempotent upsert reconciles it). */ -export function filterRowsSince(rows, sinceColumn, sinceDate) { +export function filterRowsSince(rows: D1Row[], sinceColumn: string | undefined, sinceDate: string | undefined): D1Row[] { if (!sinceDate || !sinceColumn) return rows; return rows.filter((row) => { const value = row[sinceColumn]; @@ -91,7 +108,7 @@ export function filterRowsSince(rows, sinceColumn, sinceDate) { * Build the export for ONE table: returns null for an excluded table; otherwise redacts every row, applies the * optional incremental filter, and attaches a checksum + the list of redacted columns. */ -export function buildTableExport(table, rows, opts = {}) { +export function buildTableExport(table: string, rows: D1Row[], opts: { sinceColumn?: string; sinceDate?: string } = {}): TableExport | null { if (EXCLUDED_TABLES.has(table)) return null; const filtered = filterRowsSince(rows, opts.sinceColumn, opts.sinceDate); const redacted = filtered.map((row) => redactRow(table, row)); @@ -105,9 +122,9 @@ export function buildTableExport(table, rows, opts = {}) { } /** Summarize the table exports into a manifest (row payloads omitted) the importer validates against. */ -export function buildExportManifest(tableExports, meta = {}) { +export function buildExportManifest(tableExports: Array, meta: Record = {}): ExportManifest { const tables = tableExports - .filter((entry) => entry !== null) + .filter((entry): entry is TableExport => entry !== null) .map(({ table, rowCount, redactedColumns, checksum }) => ({ table, rowCount, redactedColumns, checksum })); return { ...meta, diff --git a/scripts/export-d1-data.mjs b/scripts/export-d1-data.ts similarity index 73% rename from scripts/export-d1-data.mjs rename to scripts/export-d1-data.ts index 5ef9c0912b..2e30490538 100644 --- a/scripts/export-d1-data.mjs +++ b/scripts/export-d1-data.ts @@ -1,34 +1,42 @@ #!/usr/bin/env node // Read-only D1 → self-host export (#selfhost-migration Phase 3). Enumerates the cloud D1's tables, SELECTs each // via `wrangler d1 execute --json` (no writes to D1), and emits a redacted, checksummed, per-table JSON dump plus -// a manifest the self-host importer validates against. The data transformation lives in export-d1-core.mjs (pure, +// a manifest the self-host importer validates against. The data transformation lives in export-d1-core.ts (pure, // unit-tested); this file is the thin IO wrapper. // -// node scripts/export-d1-data.mjs --db loopover --output ./export [--remote] [--since-date 2026-06-01T00:00:00Z] [--since-column updated_at] +// tsx scripts/export-d1-data.ts --db loopover --output ./export [--remote] [--since-date 2026-06-01T00:00:00Z] [--since-column updated_at] // // --remote reads the deployed D1 (default is the local miniflare DB). --since-date does an INCREMENTAL export // (rows whose --since-column is >= the date); omit it for a full export. NEVER pass a write command. import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; -import { buildExportManifest, buildTableExport, EXCLUDED_TABLES, isSafeTableName } from "./export-d1-core.mjs"; +import { buildExportManifest, buildTableExport, EXCLUDED_TABLES, isSafeTableName, type D1Row, type TableExport } from "./export-d1-core.js"; -function parseArgs(argv) { - const args = { db: undefined, output: "./export", remote: false, sinceDate: undefined, sinceColumn: "updated_at" }; +type Args = { + db: string | undefined; + output: string; + remote: boolean; + sinceDate: string | undefined; + sinceColumn: string; +}; + +function parseArgs(argv: string[]): Args { + const args: Args = { db: undefined, output: "./export", remote: false, sinceDate: undefined, sinceColumn: "updated_at" }; for (let i = 0; i < argv.length; i += 1) { const flag = argv[i]; if (flag === "--remote") args.remote = true; else if (flag === "--db") args.db = argv[++i]; - else if (flag === "--output") args.output = argv[++i]; + else if (flag === "--output") args.output = argv[++i]!; else if (flag === "--since-date") args.sinceDate = argv[++i]; - else if (flag === "--since-column") args.sinceColumn = argv[++i]; + else if (flag === "--since-column") args.sinceColumn = argv[++i]!; } return args; } // Run a read-only SQL statement via wrangler and return the result rows. Throws on any wrangler failure so a // partial/garbled export can never be mistaken for a complete one. -function d1Query(db, remote, sql) { +function d1Query(db: string, remote: boolean, sql: string): D1Row[] { const result = spawnSync("npx", ["wrangler", "d1", "execute", db, remote ? "--remote" : "--local", "--json", "--command", sql], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, @@ -42,29 +50,31 @@ function d1Query(db, remote, sql) { return first?.results ?? []; } -function listTables(db, remote) { +function listTables(db: string, remote: boolean): string[] { const rows = d1Query(db, remote, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); // Only export plain-identifier table names (sqlite_master is trusted, but the name is interpolated into the SELECT // below, so validate it anyway). A non-conforming name is skipped loudly rather than risking an injected SELECT. - return rows.map((row) => row.name).filter((name) => { - if (!isSafeTableName(name) || EXCLUDED_TABLES.has(name)) { - if (typeof name === "string" && !isSafeTableName(name)) console.error(`skipping table with an unexpected name: ${JSON.stringify(name).slice(0, 80)}`); - return false; - } - return true; - }); + return rows + .map((row) => row.name) + .filter((name): name is string => { + if (!isSafeTableName(name) || EXCLUDED_TABLES.has(name as string)) { + if (typeof name === "string" && !isSafeTableName(name)) console.error(`skipping table with an unexpected name: ${JSON.stringify(name).slice(0, 80)}`); + return false; + } + return true; + }); } function main() { const args = parseArgs(process.argv.slice(2)); if (!args.db) { - console.error("Usage: node scripts/export-d1-data.mjs --db --output

[--remote] [--since-date ] [--since-column ]"); + console.error("Usage: tsx scripts/export-d1-data.ts --db --output [--remote] [--since-date ] [--since-column ]"); process.exit(2); } const tablesDir = join(args.output, "tables"); mkdirSync(tablesDir, { recursive: true }); - const tableExports = []; + const tableExports: TableExport[] = []; for (const table of listTables(args.db, args.remote)) { if (!isSafeTableName(table)) continue; // provably-safe interpolation: the name is a validated plain identifier const rows = d1Query(args.db, args.remote, `SELECT * FROM "${table}"`); diff --git a/scripts/extension-zip-core.d.mts b/scripts/extension-zip-core.d.mts deleted file mode 100644 index 9cae698bab..0000000000 --- a/scripts/extension-zip-core.d.mts +++ /dev/null @@ -1,3 +0,0 @@ -export function crc32(buffer: Buffer | Uint8Array): number; -export function listFiles(directory: string): string[]; -export function createStoredZip(directory: string): Buffer; diff --git a/scripts/extension-zip-core.mjs b/scripts/extension-zip-core.ts similarity index 88% rename from scripts/extension-zip-core.mjs rename to scripts/extension-zip-core.ts index 55b23f3377..1659315624 100644 --- a/scripts/extension-zip-core.mjs +++ b/scripts/extension-zip-core.ts @@ -2,7 +2,7 @@ import { readdirSync, readFileSync, statSync } from "node:fs"; import { relative, resolve } from "node:path"; // Shared byte-level ZIP writer for the browser-extension download bundles produced by -// build-extension.mjs and build-miner-extension.mjs. Both scripts hand-write a STORED +// build-extension.ts and build-miner-extension.ts. Both scripts hand-write a STORED // (uncompressed) archive so the shipped .zip needs no runtime deflate dependency; keeping // the writer here means a regression in any of the manual byte offsets is caught once, by // test/unit/build-extension-zip-script.test.ts, instead of by a user's own unzip tool. @@ -15,15 +15,15 @@ const crcTable = Array.from({ length: 256 }, (_, index) => { return value >>> 0; }); -export function crc32(buffer) { +export function crc32(buffer: Buffer | Uint8Array): number { let value = 0xffffffff; for (const byte of buffer) { - value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8); + value = crcTable[(value ^ byte) & 0xff]! ^ (value >>> 8); } return (value ^ 0xffffffff) >>> 0; } -export function listFiles(directory) { +export function listFiles(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }) .flatMap((entry) => { const path = resolve(directory, entry.name); @@ -33,10 +33,10 @@ export function listFiles(directory) { .sort(); } -export function createStoredZip(directory) { +export function createStoredZip(directory: string): Buffer { const files = listFiles(directory); - const localParts = []; - const centralParts = []; + const localParts: Buffer[] = []; + const centralParts: Buffer[] = []; let offset = 0; for (const file of files) { diff --git a/scripts/fetch-test-timing.d.mts b/scripts/fetch-test-timing.d.mts deleted file mode 100644 index 3bc26c3c85..0000000000 --- a/scripts/fetch-test-timing.d.mts +++ /dev/null @@ -1,15 +0,0 @@ -export type CodecovTestRunRow = { - filename?: string | null | undefined; - commit_sha?: string | null | undefined; - duration_seconds?: number | null | undefined; -}; - -export declare const RETRYABLE_STATUS: ReadonlySet; - -export declare function shouldRetryCodecovFetch( - status: number, - attempt: number, - maxAttempts: number, -): boolean; - -export declare function aggregateByFile(rows: CodecovTestRunRow[]): Record; diff --git a/scripts/fetch-test-timing.mjs b/scripts/fetch-test-timing.ts similarity index 79% rename from scripts/fetch-test-timing.mjs rename to scripts/fetch-test-timing.ts index fcee455491..d190fd8d3b 100644 --- a/scripts/fetch-test-timing.mjs +++ b/scripts/fetch-test-timing.ts @@ -29,29 +29,40 @@ import { writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; -export const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]); +export type CodecovTestRunRow = { + filename?: string | null | undefined; + commit_sha?: string | null | undefined; + duration_seconds?: number | null | undefined; +}; + +type CodecovTestAnalyticsPage = { + results: CodecovTestRunRow[]; + next: string | null; +}; + +export const RETRYABLE_STATUS: ReadonlySet = new Set([429, 500, 502, 503, 504]); /** True when a Codecov response status should be retried (and attempt budget remains). */ -export function shouldRetryCodecovFetch(status, attempt, maxAttempts) { +export function shouldRetryCodecovFetch(status: number, attempt: number, maxAttempts: number): boolean { return RETRYABLE_STATUS.has(status) && attempt < maxAttempts; } -export function aggregateByFile(rows) { +export function aggregateByFile(rows: readonly CodecovTestRunRow[]): Record { // Per-file duration must be averaged ACROSS RUNS, not just summed across every row: a file with many // test cases would otherwise dwarf a file with few, and a file that appears in many historical rows // (many runs) would inflate further with each additional run pooled in -- neither reflects "how long // does this file actually take in a single run." So first sum each file's rows *within* a single // commit (that commit's real per-run file duration), then average those per-commit totals across all // commits the file appears in. - const perCommitTotals = new Map(); // filename -> Map(commit_sha -> totalSeconds) + const perCommitTotals = new Map>(); for (const row of rows) { if (!row.filename || row.duration_seconds == null) continue; if (!perCommitTotals.has(row.filename)) perCommitTotals.set(row.filename, new Map()); - const commits = perCommitTotals.get(row.filename); + const commits = perCommitTotals.get(row.filename)!; commits.set(row.commit_sha, (commits.get(row.commit_sha) ?? 0) + row.duration_seconds); } - const averages = {}; + const averages: Record = {}; for (const [filename, commits] of perCommitTotals) { const totals = [...commits.values()]; averages[filename] = totals.reduce((sum, value) => sum + value, 0) / totals.length; @@ -59,12 +70,12 @@ export function aggregateByFile(rows) { return averages; } -async function fetchWithRetry(url, token, maxAttempts = 4) { +async function fetchWithRetry(url: string, token: string, maxAttempts = 4): Promise { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const response = await fetch(url, { headers: { Authorization: `bearer ${token}`, Accept: "application/json" }, }); - if (response.ok) return response.json(); + if (response.ok) return response.json() as Promise; if (!shouldRetryCodecovFetch(response.status, attempt, maxAttempts)) { throw new Error(`Codecov API error ${response.status} on ${url}: ${await response.text()}`); } @@ -75,9 +86,19 @@ async function fetchWithRetry(url, token, maxAttempts = 4) { throw new Error("unreachable"); } -async function fetchAllTestRuns({ owner, repoName, token, maxPages }) { - const rows = []; - let url = `https://api.codecov.io/api/v2/gh/${owner}/repos/${repoName}/test-analytics/?branch=main&page_size=100`; +async function fetchAllTestRuns({ + owner, + repoName, + token, + maxPages, +}: { + owner: string; + repoName: string; + token: string; + maxPages: number; +}): Promise<{ rows: CodecovTestRunRow[]; truncated: boolean }> { + const rows: CodecovTestRunRow[] = []; + let url: string | null = `https://api.codecov.io/api/v2/gh/${owner}/repos/${repoName}/test-analytics/?branch=main&page_size=100`; let pages = 0; while (url && pages < maxPages) { const body = await fetchWithRetry(url, token); @@ -99,7 +120,7 @@ async function main() { const token = process.env.CODECOV_API_TOKEN; if (!token) throw new Error("CODECOV_API_TOKEN is required"); - const { rows, truncated } = await fetchAllTestRuns({ owner, repoName, token, maxPages }); + const { rows, truncated } = await fetchAllTestRuns({ owner: owner!, repoName: repoName!, token, maxPages }); const averageSecondsByFile = aggregateByFile(rows); const report = { diff --git a/scripts/forbidden-content.d.mts b/scripts/forbidden-content.d.mts deleted file mode 100644 index b03f4933d7..0000000000 --- a/scripts/forbidden-content.d.mts +++ /dev/null @@ -1 +0,0 @@ -export const FORBIDDEN_CONTENT: RegExp; diff --git a/scripts/forbidden-content.mjs b/scripts/forbidden-content.ts similarity index 97% rename from scripts/forbidden-content.mjs rename to scripts/forbidden-content.ts index bfef0136ef..e1cdcbcf6b 100644 --- a/scripts/forbidden-content.mjs +++ b/scripts/forbidden-content.ts @@ -14,7 +14,7 @@ // for #7433, and seed_or_mnemonic/bittensor_key are documented in secret-patterns.ts as weak, false-positive- // prone heuristics intentionally excluded from HARD_SECRET_KINDS (an ordinary `coldkey:` / `hotkey =` line or // the word "mnemonic" in Bittensor docs is not a leaked credential). -export const FORBIDDEN_CONTENT = new RegExp( +export const FORBIDDEN_CONTENT: RegExp = new RegExp( [ // Already covered before #7433 (unchanged shapes): "BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY", diff --git a/scripts/gen-cf-typegen.d.mts b/scripts/gen-cf-typegen.d.mts deleted file mode 100644 index 17fb64ccb5..0000000000 --- a/scripts/gen-cf-typegen.d.mts +++ /dev/null @@ -1,7 +0,0 @@ -export const OUTPUT_PATH: string; - -export function resolveLocalWranglerBin(): string; - -export function formatCfTypegenOutput(source: string): string; - -export function genCfTypegen(options?: { check?: boolean }): { changed: boolean | undefined }; diff --git a/scripts/gen-cf-typegen.mjs b/scripts/gen-cf-typegen.ts similarity index 89% rename from scripts/gen-cf-typegen.mjs rename to scripts/gen-cf-typegen.ts index 3f51b9bb8a..dd97ab48fa 100644 --- a/scripts/gen-cf-typegen.mjs +++ b/scripts/gen-cf-typegen.ts @@ -16,10 +16,10 @@ const require = createRequire(import.meta.url); * wrangler's own package.json restricts its `exports` map, so the bin subpath itself isn't directly * requireable -- resolve the package root via its (always-exported) package.json instead, then read * the real bin path from there rather than hand-hardcoding "./bin/wrangler.js". */ -export function resolveLocalWranglerBin() { +export function resolveLocalWranglerBin(): string { const pkgJsonPath = require.resolve("wrangler/package.json"); - const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")); - const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.wrangler; + const pkg: { bin: string | Record } = JSON.parse(readFileSync(pkgJsonPath, "utf8")); + const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.wrangler!; return join(dirname(pkgJsonPath), binRelativePath); } @@ -41,15 +41,13 @@ const PICK_RE = /Pick>/; * whatever path argument was passed, which check mode intentionally sets to a different temp path than * the real OUTPUT_PATH — without this, check mode would always report false staleness on that line alone), * then reformat the single-line ProcessEnv Pick union into one sorted entry per line. */ -export function formatCfTypegenOutput(source) { - const stripped = source - .replace(/[ \t]+$/gm, "") - .replace(/(Generated by Wrangler by running `wrangler types)[^`]*`/, "$1`"); +export function formatCfTypegenOutput(source: string): string { + const stripped = source.replace(/[ \t]+$/gm, "").replace(/(Generated by Wrangler by running `wrangler types)[^`]*`/, "$1`"); const match = stripped.match(PICK_RE); if (!match) { throw new Error(`Could not find the ProcessEnv Pick union in the generated output to reformat.`); } - const keys = match[1] + const keys = match[1]! .split(" | ") .map((quoted) => quoted.slice(1, -1)) .sort((a, b) => a.localeCompare(b)); @@ -57,7 +55,7 @@ export function formatCfTypegenOutput(source) { return stripped.replace(PICK_RE, `Pick>`); } -export function genCfTypegen({ check = false } = {}) { +export function genCfTypegen({ check = false } = {}): { changed: boolean | undefined } { const target = check ? TEMP_PATH : OUTPUT_PATH; try { execFileSync(process.execPath, [resolveLocalWranglerBin(), "types", target], { stdio: "inherit" }); @@ -73,7 +71,7 @@ export function genCfTypegen({ check = false } = {}) { } } -function main(argv) { +function main(argv: string[]) { const check = argv.includes("--check"); const { changed } = genCfTypegen({ check }); if (check) { diff --git a/scripts/gen-command-reference.d.mts b/scripts/gen-command-reference.d.mts deleted file mode 100644 index 990803fb95..0000000000 --- a/scripts/gen-command-reference.d.mts +++ /dev/null @@ -1,42 +0,0 @@ -export type CommandCatalogEntry = { - id: string; - title: string; - description: string; -}; - -export type CommandCatalogOptions = { - rootDir?: string; - sourcePath?: string; -}; - -export type WriteCommandReferenceOptions = CommandCatalogOptions & { - outputPath?: string; - check?: boolean; -}; - -export declare const DEFAULT_SOURCE_PATH: string; -export declare const DEFAULT_OUTPUT_PATH: string; - -export declare function extractCatalogEntries(sourceText: string, catalogConstName: string): CommandCatalogEntry[]; - -export declare function renderCommandList(entries: CommandCatalogEntry[]): string; - -export declare function collectCommandCatalogs(options?: CommandCatalogOptions): { - publicCommands: CommandCatalogEntry[]; - maintainerCommands: CommandCatalogEntry[]; - actionCommands: CommandCatalogEntry[]; -}; - -export declare function renderCommandReferenceModule(catalogs: { - publicCommands: CommandCatalogEntry[]; - maintainerCommands: CommandCatalogEntry[]; - actionCommands: CommandCatalogEntry[]; -}): string; - -export declare function writeCommandReference(options?: WriteCommandReferenceOptions): { - changed: boolean; - outputPath: string; - publicCommands: CommandCatalogEntry[]; - maintainerCommands: CommandCatalogEntry[]; - actionCommands: CommandCatalogEntry[]; -}; diff --git a/scripts/gen-command-reference.mjs b/scripts/gen-command-reference.ts similarity index 73% rename from scripts/gen-command-reference.mjs rename to scripts/gen-command-reference.ts index c8e3dbe8ee..96b4523fb5 100644 --- a/scripts/gen-command-reference.mjs +++ b/scripts/gen-command-reference.ts @@ -3,7 +3,7 @@ // src/github/commands.ts's PUBLIC_MENTION_COMMAND_CATALOG and MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG // -- so docs pages render the real command list instead of hand-copying it (a hand-copy is exactly how // the 9-command maintainer-only queue-digest family went completely undocumented; see #3046). Mirrors -// scripts/gen-selfhost-env-reference.mjs's generate/--check dual-mode convention. +// scripts/gen-selfhost-env-reference.ts's generate/--check dual-mode convention. import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; @@ -20,33 +20,49 @@ const ACTION_CATALOG_NAME = "LOOPOVER_ACTION_COMMAND_CATALOG"; // loudly instead of silently writing an empty/tiny reference (mirrors check-docs-drift.mjs's own floor). const MIN_TOTAL_COMMANDS = 15; +export type CommandCatalogEntry = { + id: string; + title: string; + description: string; +}; + +export type CommandCatalogOptions = { + rootDir?: string; + sourcePath?: string; +}; + +export type WriteCommandReferenceOptions = CommandCatalogOptions & { + outputPath?: string; + check?: boolean; +}; + /** Find the array literal assigned to `const = [ ... ] as const;` (non-greedy up to * the FIRST `] as const;` after the const name -- catalogs in commands.ts never nest another * `] as const;` inside themselves, so the first close is always the right one) and extract every * `{ id: "...", title: "...", description: "..." }` entry from within that slice, in source order. * Scoped to the named catalog's own slice so two catalogs in the same file never bleed into each * other's entries. */ -export function extractCatalogEntries(sourceText, catalogConstName) { +export function extractCatalogEntries(sourceText: string, catalogConstName: string): CommandCatalogEntry[] { const catalogPattern = new RegExp(`const\\s+${catalogConstName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s*const;`); const catalogMatch = catalogPattern.exec(sourceText); if (!catalogMatch) return []; const entryPattern = /id:\s*"([^"]+)"\s*,\s*title:\s*"([^"]+)"\s*,\s*description:\s*"([^"]+)"/g; - return [...catalogMatch[1].matchAll(entryPattern)].map((match) => ({ - id: match[1], - title: match[2], - description: match[3], + return [...catalogMatch[1]!.matchAll(entryPattern)].map((match) => ({ + id: match[1]!, + title: match[2]!, + description: match[3]!, })); } /** Renders a catalog's entries as the same `@loopover ` line format the docs pages already * hand-write, newline-joined, in catalog order. */ -export function renderCommandList(entries) { +export function renderCommandList(entries: readonly CommandCatalogEntry[]): string { return entries.map((entry) => `@loopover ${entry.id}`).join("\n"); } const PRETTIER_PRINT_WIDTH = 100; -function renderDescriptionField(description) { +function renderDescriptionField(description: string): string { const inline = ` description: ${JSON.stringify(description)},`; if (inline.length <= PRETTIER_PRINT_WIDTH) { return inline; @@ -54,15 +70,18 @@ function renderDescriptionField(description) { return ` description:\n ${JSON.stringify(description)},`; } -function renderCommandEntries(entries) { +function renderCommandEntries(entries: readonly CommandCatalogEntry[]): string { const rows = entries.map( - (entry) => - ` {\n id: ${JSON.stringify(entry.id)},\n title: ${JSON.stringify(entry.title)},\n${renderDescriptionField(entry.description)}\n },`, + (entry) => ` {\n id: ${JSON.stringify(entry.id)},\n title: ${JSON.stringify(entry.title)},\n${renderDescriptionField(entry.description)}\n },`, ); return `[\n${rows.join("\n")}\n]`; } -export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = DEFAULT_SOURCE_PATH } = {}) { +export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = DEFAULT_SOURCE_PATH }: CommandCatalogOptions = {}): { + publicCommands: CommandCatalogEntry[]; + maintainerCommands: CommandCatalogEntry[]; + actionCommands: CommandCatalogEntry[]; +} { const sourceText = readFileSync(resolve(rootDir, sourcePath), "utf8"); const publicCommands = extractCatalogEntries(sourceText, PUBLIC_CATALOG_NAME); const maintainerCommands = extractCatalogEntries(sourceText, MAINTAINER_CATALOG_NAME); @@ -81,8 +100,16 @@ export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = D return { publicCommands, maintainerCommands, actionCommands }; } -export function renderCommandReferenceModule({ publicCommands, maintainerCommands, actionCommands }) { - const body = `// Generated by scripts/gen-command-reference.mjs. Do not edit manually. +export function renderCommandReferenceModule({ + publicCommands, + maintainerCommands, + actionCommands, +}: { + publicCommands: readonly CommandCatalogEntry[]; + maintainerCommands: readonly CommandCatalogEntry[]; + actionCommands: readonly CommandCatalogEntry[]; +}): string { + const body = `// Generated by scripts/gen-command-reference.ts. Do not edit manually. // Regenerate via \`npm run command-reference\`. export const PUBLIC_COMMAND_ENTRIES = ${renderCommandEntries(publicCommands)} as const; @@ -102,12 +129,13 @@ export const ACTION_COMMAND_LIST = return body; } -export function writeCommandReference({ - rootDir = process.cwd(), - sourcePath = DEFAULT_SOURCE_PATH, - outputPath = DEFAULT_OUTPUT_PATH, - check = false, -} = {}) { +export function writeCommandReference({ rootDir = process.cwd(), sourcePath = DEFAULT_SOURCE_PATH, outputPath = DEFAULT_OUTPUT_PATH, check = false }: WriteCommandReferenceOptions = {}): { + changed: boolean; + outputPath: string; + publicCommands: CommandCatalogEntry[]; + maintainerCommands: CommandCatalogEntry[]; + actionCommands: CommandCatalogEntry[]; +} { const { publicCommands, maintainerCommands, actionCommands } = collectCommandCatalogs({ rootDir, sourcePath }); const output = renderCommandReferenceModule({ publicCommands, maintainerCommands, actionCommands }); const absOutput = resolve(rootDir, outputPath); @@ -120,7 +148,7 @@ export function writeCommandReference({ return { changed, outputPath, publicCommands, maintainerCommands, actionCommands }; } -function main(argv) { +function main(argv: readonly string[]) { const check = argv.includes("--check"); const result = writeCommandReference({ check }); if (check && result.changed) { diff --git a/scripts/gen-selfhost-env-reference.d.mts b/scripts/gen-selfhost-env-reference.d.mts deleted file mode 100644 index c52b6aa87e..0000000000 --- a/scripts/gen-selfhost-env-reference.d.mts +++ /dev/null @@ -1,33 +0,0 @@ -export type SelfHostEnvReferenceRow = { - name: string; - firstReference: string; -}; - -export type SelfHostEnvReferenceOptions = { - rootDir?: string; - sourceRoots?: readonly string[]; -}; - -export type WriteSelfHostEnvReferenceOptions = SelfHostEnvReferenceOptions & { - outputPath?: string; - check?: boolean; -}; - -export declare const DEFAULT_OUTPUT_PATH: string; -export declare const DEFAULT_SOURCE_ROOTS: readonly string[]; - -export declare function collectSelfHostEnvVars( - options?: SelfHostEnvReferenceOptions, -): SelfHostEnvReferenceRow[]; - -export declare function renderSelfHostEnvReferenceMarkdown(rows: SelfHostEnvReferenceRow[]): string; - -export declare function renderSelfHostEnvReferenceModule(rows: SelfHostEnvReferenceRow[]): string; - -export declare function writeSelfHostEnvReference( - options?: WriteSelfHostEnvReferenceOptions, -): { - changed: boolean; - outputPath: string; - rows: SelfHostEnvReferenceRow[]; -}; diff --git a/scripts/gen-selfhost-env-reference.mjs b/scripts/gen-selfhost-env-reference.ts similarity index 59% rename from scripts/gen-selfhost-env-reference.mjs rename to scripts/gen-selfhost-env-reference.ts index b8d9af02c8..61ce74f245 100644 --- a/scripts/gen-selfhost-env-reference.mjs +++ b/scripts/gen-selfhost-env-reference.ts @@ -36,11 +36,23 @@ const INJECTED_BINDING_NAMES = new Set([ "WEBHOOKS", ]); -export function collectSelfHostEnvVars({ - rootDir = process.cwd(), - sourceRoots = DEFAULT_SOURCE_ROOTS, -} = {}) { - const rows = new Map(); +export type SelfHostEnvReferenceRow = { + name: string; + firstReference: string; +}; + +export type SelfHostEnvReferenceOptions = { + rootDir?: string; + sourceRoots?: readonly string[]; +}; + +export type WriteSelfHostEnvReferenceOptions = SelfHostEnvReferenceOptions & { + outputPath?: string; + check?: boolean; +}; + +export function collectSelfHostEnvVars({ rootDir = process.cwd(), sourceRoots = DEFAULT_SOURCE_ROOTS }: SelfHostEnvReferenceOptions = {}): SelfHostEnvReferenceRow[] { + const rows = new Map(); for (const file of sourceFiles(rootDir, sourceRoots)) { const abs = resolve(rootDir, file); for (const read of collectEnvReads(readFileSync(abs, "utf8"), file)) { @@ -50,44 +62,37 @@ export function collectSelfHostEnvVars({ return [...rows.values()].sort((a, b) => a.name.localeCompare(b.name)); } +type EnvRead = { name: string }; + // Deliberately file-only, not `file:line` (#env-reference-churn) -- a line number makes the generated output // change whenever ANYTHING above an existing read shifts, so two unrelated PRs touching the same source file // produce two different regenerated rows and collide on rebase. The file path only changes when a read is // actually added/removed/moved to a different file, which is the only case that should ever require // regenerating this doc. -function collectEnvReads(source, fileName) { +function collectEnvReads(source: string, fileName: string): EnvRead[] { const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName)); - const reads = []; - const addRead = (name) => { + const reads: EnvRead[] = []; + const addRead = (name: string) => { if (!ENV_NAME_RE.test(name) || INJECTED_BINDING_NAMES.has(name)) return; reads.push({ name }); }; - const visit = (node) => { + const visit = (node: ts.Node) => { if (ts.isPropertyAccessExpression(node) && isEnvContainer(node.expression)) { addRead(node.name.text); - } else if ( - ts.isElementAccessExpression(node) && - isEnvContainer(node.expression) && - ts.isStringLiteralLike(node.argumentExpression) - ) { + } else if (ts.isElementAccessExpression(node) && isEnvContainer(node.expression) && ts.isStringLiteralLike(node.argumentExpression)) { addRead(node.argumentExpression.text); - } else if ( - ts.isVariableDeclaration(node) && - ts.isObjectBindingPattern(node.name) && - node.initializer && - isEnvContainer(node.initializer) - ) { + } else if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && isEnvContainer(node.initializer)) { for (const element of node.name.elements) { const name = bindingElementName(element); if (name) addRead(name); } } else if (ts.isCallExpression(node) && isStaticEnvHelperCall(node)) { - addRead(node.arguments[1].text); + addRead((node.arguments[1] as ts.StringLiteralLike).text); } else if (ts.isCallExpression(node) && isProcessEnvNameHelperCall(node)) { - addRead(node.arguments[0].text); + addRead((node.arguments[0] as ts.StringLiteralLike).text); } else if (ts.isCallExpression(node) && isEnvNameLiteralArgHelperCall(node)) { - const argIndex = ENV_NAME_LITERAL_ARG_HELPERS.get(node.expression.text); - addRead(node.arguments[argIndex].text); + const argIndex = ENV_NAME_LITERAL_ARG_HELPERS.get((node.expression as ts.Identifier).text)!; + addRead((node.arguments[argIndex] as ts.StringLiteralLike).text); } ts.forEachChild(node, visit); }; @@ -95,13 +100,13 @@ function collectEnvReads(source, fileName) { return reads; } -function isStaticEnvHelperCall(node) { +function isStaticEnvHelperCall(node: ts.CallExpression): boolean { return ( ts.isIdentifier(node.expression) && node.expression.text === "envString" && node.arguments.length >= 2 && - isEnvContainer(node.arguments[0]) && - ts.isStringLiteralLike(node.arguments[1]) + isEnvContainer(node.arguments[0]!) && + ts.isStringLiteralLike(node.arguments[1]!) ); } @@ -118,26 +123,17 @@ const ENV_NAME_LITERAL_ARG_HELPERS = new Map([ ["createCliProvider", 1], ]); -function isProcessEnvNameHelperCall(node) { - return ( - ts.isIdentifier(node.expression) && - PROCESS_ENV_NAME_HELPERS.has(node.expression.text) && - node.arguments.length >= 1 && - ts.isStringLiteralLike(node.arguments[0]) - ); +function isProcessEnvNameHelperCall(node: ts.CallExpression): boolean { + return ts.isIdentifier(node.expression) && PROCESS_ENV_NAME_HELPERS.has(node.expression.text) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0]!); } -function isEnvNameLiteralArgHelperCall(node) { +function isEnvNameLiteralArgHelperCall(node: ts.CallExpression): boolean { if (!ts.isIdentifier(node.expression)) return false; const argIndex = ENV_NAME_LITERAL_ARG_HELPERS.get(node.expression.text); - return ( - argIndex !== undefined && - node.arguments.length > argIndex && - ts.isStringLiteralLike(node.arguments[argIndex]) - ); + return argIndex !== undefined && node.arguments.length > argIndex && ts.isStringLiteralLike(node.arguments[argIndex]!); } -function bindingElementName(element) { +function bindingElementName(element: ts.BindingElement): string | null { const candidate = element.propertyName ?? element.name; if (ts.isIdentifier(candidate) || ts.isStringLiteralLike(candidate)) return candidate.text; return null; @@ -146,50 +142,40 @@ function bindingElementName(element) { // Unwraps `(x)` and `x as T` (including chained casts like `env as unknown as Record`, the // pattern src/services/notify-discord.ts uses to read an env key TypeScript's Env type doesn't declare) so // isEnvContainer sees the underlying identifier/property-access instead of the cast wrapper (#2907). -function unwrapEnvExpression(node) { +function unwrapEnvExpression(node: ts.Expression): ts.Expression { if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) return unwrapEnvExpression(node.expression); return node; } -function isEnvContainer(rawNode) { +function isEnvContainer(rawNode: ts.Expression): boolean { const node = unwrapEnvExpression(rawNode); if (ts.isIdentifier(node)) return node.text === "env"; return ( ts.isPropertyAccessExpression(node) && node.name.text === "env" && - ((ts.isIdentifier(node.expression) && (node.expression.text === "process" || node.expression.text === "c")) || - isEnvContainer(node.expression)) + ((ts.isIdentifier(node.expression) && (node.expression.text === "process" || node.expression.text === "c")) || isEnvContainer(node.expression)) ); } -function scriptKindFor(fileName) { +function scriptKindFor(fileName: string): ts.ScriptKind { if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX; if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX; if (fileName.endsWith(".js") || fileName.endsWith(".mjs") || fileName.endsWith(".cjs")) return ts.ScriptKind.JS; return ts.ScriptKind.TS; } -export function renderSelfHostEnvReferenceMarkdown(rows) { - return [ - "| Name | First reference |", - "| --- | --- |", - ...rows.map((row) => `| \`${row.name}\` | \`${row.firstReference}\` |`), - ].join("\n"); +export function renderSelfHostEnvReferenceMarkdown(rows: readonly SelfHostEnvReferenceRow[]): string { + return ["| Name | First reference |", "| --- | --- |", ...rows.map((row) => `| \`${row.name}\` | \`${row.firstReference}\` |`)].join("\n"); } -export function renderSelfHostEnvReferenceModule(rows) { +export function renderSelfHostEnvReferenceModule(rows: readonly SelfHostEnvReferenceRow[]): string { const markdown = renderSelfHostEnvReferenceMarkdown(rows); - const rowLines = rows - .map( - (row) => - ` {\n name: ${JSON.stringify(row.name)},\n firstReference: ${JSON.stringify(row.firstReference)},\n },`, - ) - .join("\n"); + const rowLines = rows.map((row) => ` {\n name: ${JSON.stringify(row.name)},\n firstReference: ${JSON.stringify(row.firstReference)},\n },`).join("\n"); const markdownLines = markdown .split("\n") .map((line) => ` ${JSON.stringify(line)},`) .join("\n"); - return `// Generated by scripts/gen-selfhost-env-reference.mjs. Do not edit manually. + return `// Generated by scripts/gen-selfhost-env-reference.ts. Do not edit manually. export type SelfHostEnvReferenceRow = { name: string; firstReference: string; @@ -205,12 +191,11 @@ ${markdownLines} `; } -export function writeSelfHostEnvReference({ - rootDir = process.cwd(), - outputPath = DEFAULT_OUTPUT_PATH, - sourceRoots = DEFAULT_SOURCE_ROOTS, - check = false, -} = {}) { +export function writeSelfHostEnvReference({ rootDir = process.cwd(), outputPath = DEFAULT_OUTPUT_PATH, sourceRoots = DEFAULT_SOURCE_ROOTS, check = false }: WriteSelfHostEnvReferenceOptions = {}): { + changed: boolean; + outputPath: string; + rows: SelfHostEnvReferenceRow[]; +} { const rows = collectSelfHostEnvVars({ rootDir, sourceRoots }); const output = renderSelfHostEnvReferenceModule(rows); const absOutput = resolve(rootDir, outputPath); @@ -223,8 +208,8 @@ export function writeSelfHostEnvReference({ return { changed, outputPath, rows }; } -function sourceFiles(rootDir, sourceRoots) { - const files = []; +function sourceFiles(rootDir: string, sourceRoots: readonly string[]): string[] { + const files: string[] = []; for (const sourceRoot of sourceRoots) { const abs = resolve(rootDir, sourceRoot); if (!existsSync(abs)) continue; @@ -243,40 +228,52 @@ function sourceFiles(rootDir, sourceRoots) { return files; } -function walkSourceFiles(dir) { - const files = []; +const COMPILED_JS_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); + +function walkSourceFiles(dir: string): string[] { + const files: string[] = []; const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); + // A same-basename .ts/.tsx sibling in this same directory listing means the .js/.mjs/.cjs file is that + // source's compiled output, not independent source -- skip it. Once a package's compiled output is + // gitignored and built on demand rather than committed (#7290/#7291/#7705), whether that .js physically + // exists on disk varies by environment (a dev machine that happens to have already run a build vs. a + // fresh CI checkout before its own build step runs), and scanning both would let firstReference + // attribution silently flip between the .js and .ts path depending on which environment generated the + // committed reference doc. A genuinely source-only .mjs with no .ts sibling (e.g. scripts/build- + // selfhost.mjs, reached via a file source root rather than this directory walk, but the same rule would + // apply if one were ever added to a walked directory) is unaffected -- it has nothing to be shadowed by. + const tsBasenames = new Set( + entries.filter((entry) => entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))).map((entry) => entry.name.slice(0, entry.name.lastIndexOf("."))), + ); for (const entry of entries) { const abs = resolve(dir, entry.name); if (entry.isDirectory()) { files.push(...walkSourceFiles(abs)); } else if (entry.isFile() && isSupportedSourceFile(abs)) { + const ext = extname(entry.name); + if (COMPILED_JS_EXTENSIONS.has(ext) && tsBasenames.has(entry.name.slice(0, -ext.length))) continue; files.push(abs); } } return files; } -function isSupportedSourceFile(file) { +function isSupportedSourceFile(file: string): boolean { return SOURCE_FILE_EXTENSIONS.has(extname(file)); } -function toPosixPath(path) { +function toPosixPath(path: string): string { return path.split(sep).join("/"); } -function main(argv) { +function main(argv: readonly string[]) { const check = argv.includes("--check"); const result = writeSelfHostEnvReference({ check }); if (check && result.changed) { - process.stderr.write( - `gen-selfhost-env-reference: ${result.outputPath} is stale; run npm run selfhost:env-reference.\n`, - ); + process.stderr.write(`gen-selfhost-env-reference: ${result.outputPath} is stale; run npm run selfhost:env-reference.\n`); process.exit(1); } - process.stdout.write( - `gen-selfhost-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`, - ); + process.stdout.write(`gen-selfhost-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`); } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { diff --git a/scripts/generate-mcp-changelog.d.mts b/scripts/generate-mcp-changelog.d.mts deleted file mode 100644 index 2e02fd8cfe..0000000000 --- a/scripts/generate-mcp-changelog.d.mts +++ /dev/null @@ -1,19 +0,0 @@ -import type { McpReleaseCommit } from "./mcp-release-core.d.mts"; - -export function generateMcpChangelog(input?: { - output?: string; - version?: string; - generatedAt?: string; - baseTag?: string | null; - dryRun?: boolean; -}): { - output: string; - version: string; - baseTag: string | null; - commits: McpReleaseCommit[]; - changelog: string; -}; - -export function readReleasePrepEntries(input: { baseTag: string | null; targetVersion: string }): McpReleaseCommit[]; -export function dependencyChange(name: string, previousDependencies?: Record | undefined, currentDependencies?: Record | undefined): string | null; -export function readConstant(source: string | null, constantName: string): string | null; diff --git a/scripts/generate-mcp-changelog.mjs b/scripts/generate-mcp-changelog.ts similarity index 75% rename from scripts/generate-mcp-changelog.mjs rename to scripts/generate-mcp-changelog.ts index cdae71919f..4b5e8ccf10 100644 --- a/scripts/generate-mcp-changelog.mjs +++ b/scripts/generate-mcp-changelog.ts @@ -1,14 +1,27 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; -import { latestSemverTag, renderMcpChangelog, selectMcpReleaseCommits } from "./mcp-release-core.mjs"; +import { latestSemverTag, renderMcpChangelog, selectMcpReleaseCommits, type McpReleaseCommit } from "./mcp-release-core.js"; const defaultOutput = "packages/loopover-mcp/CHANGELOG.md"; const defaultPackageJson = "packages/loopover-mcp/package.json"; -export function generateMcpChangelog({ output = defaultOutput, version, generatedAt, baseTag, dryRun = false } = {}) { +type GenerateMcpChangelogOptions = { + output?: string; + version?: string; + generatedAt?: string; + baseTag?: string | null; + dryRun?: boolean; +}; + +export function generateMcpChangelog({ output = defaultOutput, version, generatedAt, baseTag, dryRun = false }: GenerateMcpChangelogOptions = {}): { + output: string; + version: string; + baseTag: string | null; + commits: McpReleaseCommit[]; + changelog: string; +} { const targetVersion = version ?? readPackageVersion(defaultPackageJson); const resolvedBaseTag = baseTag ?? latestTagBeforeVersion(targetVersion)?.tag ?? null; const commits = resolvedBaseTag ? readCommits(`${resolvedBaseTag}..HEAD`) : readCommits("HEAD"); @@ -36,18 +49,18 @@ function main() { } } -function parseArgs(argv) { - const args = {}; +function parseArgs(argv: readonly string[]): GenerateMcpChangelogOptions { + const args: GenerateMcpChangelogOptions = {}; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--output") { - args.output = argv[++index]; + args.output = argv[++index]!; } else if (arg === "--version") { - args.version = argv[++index]; + args.version = argv[++index]!; } else if (arg === "--date") { - args.generatedAt = argv[++index]; + args.generatedAt = argv[++index]!; } else if (arg === "--base-tag") { - args.baseTag = argv[++index]; + args.baseTag = argv[++index]!; } else if (arg === "--dry-run") { args.dryRun = true; } else { @@ -57,12 +70,14 @@ function parseArgs(argv) { return args; } -function latestTagBeforeVersion(version) { - const tags = git(["tag", "--list", "mcp-v*", "--sort=-v:refname"]).split("\n").filter(Boolean); +function latestTagBeforeVersion(version: string) { + const tags = git(["tag", "--list", "mcp-v*", "--sort=-v:refname"]) + .split("\n") + .filter(Boolean); return latestSemverTag(tags.filter((tag) => tag.replace(/^mcp-v/, "") !== version)); } -function readCommits(revisionRange) { +function readCommits(revisionRange: string): McpReleaseCommit[] { const format = "%x1e%H%x1f%s%x1f%B"; const logOutput = git(["log", "--reverse", "--no-merges", `--format=${format}`, revisionRange]); return logOutput @@ -72,25 +87,27 @@ function readCommits(revisionRange) { .map((entry) => { const [sha, subject, ...bodyParts] = entry.split("\x1f"); return { - sha, + sha: sha!, subject: subject?.split("\n")[0] ?? "", body: bodyParts.join("\x1f"), - files: readCommitFiles(sha), + files: readCommitFiles(sha!), }; }); } -function readCommitFiles(sha) { - return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]).split("\n").filter(Boolean); +function readCommitFiles(sha: string): string[] { + return git(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]) + .split("\n") + .filter(Boolean); } -function readPackageVersion(path) { +function readPackageVersion(path: string): string { return JSON.parse(readFileSync(path, "utf8")).version; } -export function readReleasePrepEntries({ baseTag, targetVersion }) { +export function readReleasePrepEntries({ baseTag, targetVersion }: { baseTag: string | null; targetVersion: string }): McpReleaseCommit[] { if (!baseTag) return []; - const entries = []; + const entries: McpReleaseCommit[] = []; const previousMcpPackage = readJsonAt(baseTag, defaultPackageJson); const currentMcpPackage = JSON.parse(readFileSync(defaultPackageJson, "utf8")); const previousRootPackage = readJsonAt(baseTag, "package.json"); @@ -128,23 +145,23 @@ export function readReleasePrepEntries({ baseTag, targetVersion }) { return entries; } -export function dependencyChange(name, previousDependencies = {}, currentDependencies = {}) { +export function dependencyChange(name: string, previousDependencies: Record | undefined = {}, currentDependencies: Record | undefined = {}): string | null { const previousVersion = previousDependencies?.[name]; const currentVersion = currentDependencies?.[name]; if (!previousVersion || !currentVersion || previousVersion === currentVersion) return null; return `${name} ${previousVersion} -> ${currentVersion}`; } -export function readConstant(source, constantName) { - return new RegExp(`export const ${constantName} = "([^"]+)"`).exec(source)?.[1] ?? null; +export function readConstant(source: string | null, constantName: string): string | null { + return new RegExp(`export const ${constantName} = "([^"]+)"`).exec(source ?? "")?.[1] ?? null; } -function readJsonAt(ref, path) { +function readJsonAt(ref: string, path: string): any { const source = readFileAt(ref, path); return source ? JSON.parse(source) : null; } -function readFileAt(ref, path) { +function readFileAt(ref: string, path: string): string | null { try { return git(["show", `${ref}:${path}`]); } catch { @@ -152,25 +169,24 @@ function readFileAt(ref, path) { } } -function existingReleaseDate(changelog, version) { +function existingReleaseDate(changelog: string, version: string): string | null { const match = new RegExp(`^## mcp-v${escapeRegExp(version)} - (\\d{4}-\\d{2}-\\d{2})$`, "m").exec(changelog); return match?.[1] ?? null; } -function todayIsoDate() { +function todayIsoDate(): string { return new Date().toISOString().slice(0, 10); } -function git(args) { +function git(args: readonly string[]): string { return execFileSync("git", args, { encoding: "utf8", maxBuffer: 1024 * 1024 * 200 }); } -function escapeRegExp(value) { +function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -const entrypointPath = process.argv[1] ? resolve(process.argv[1]) : ""; -if (import.meta.url === pathToFileURL(entrypointPath).href) { +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { try { main(); } catch (error) { diff --git a/scripts/gittensor-impact-card.d.mts b/scripts/gittensor-impact-card.d.mts deleted file mode 100644 index 11aaf3735f..0000000000 --- a/scripts/gittensor-impact-card.d.mts +++ /dev/null @@ -1,14 +0,0 @@ -// Deliberately not `typeof fetch`: the global fetch type (in a Cloudflare Workers-typed environment) is -// overloaded to accept URL/RequestInfo/CfProperties, which a plainly-typed vi.fn() mock can't satisfy under -// strict function-type checking (see scripts/load-test-worker.d.mts's identical note). Both functions here -// only ever call fetchImpl with a string URL and a plain {headers?, signal} init. -export type ImpactCardFetch = ( - url: string, - init?: { headers?: Record; signal?: AbortSignal }, -) => Promise<{ ok?: boolean; status?: number; json?: () => Promise; text?: () => Promise }>; - -export declare const GITTENSOR_IMPACT_CARD_FETCH_TIMEOUT_MS: number; - -export declare function fetchJson(url: string, fetchImpl?: ImpactCardFetch): Promise; - -export declare function fetchGtLogoSvg(fetchImpl?: ImpactCardFetch): Promise; diff --git a/scripts/gittensor-impact-card.mjs b/scripts/gittensor-impact-card.ts similarity index 62% rename from scripts/gittensor-impact-card.mjs rename to scripts/gittensor-impact-card.ts index 24b9dd5946..ceedc22726 100644 --- a/scripts/gittensor-impact-card.mjs +++ b/scripts/gittensor-impact-card.ts @@ -6,17 +6,17 @@ // rendering (its per-repo data-fetch approach inspired this, but the visual // design here is our own, matching apps/loopover-ui/src/styles.css). // -// Usage: node scripts/gittensor-impact-card.mjs +// Usage: node scripts/gittensor-impact-card.ts import { readFileSync, writeFileSync } from "node:fs"; -import { fileURLToPath, pathToFileURL } from "node:url"; import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; const WEEKS = 12; // Bounds every outbound fetch in this script so a hung api.gittensor.io/gittensor.io connection can't block // the README-card regeneration job indefinitely -- matches the AbortSignal.timeout discipline every other // external-fetch script in this repo already follows (src/github/client.ts's GITHUB_FETCH_TIMEOUT_MS, -// scripts/check-mcp-release-due.mjs's GITHUB_REQUEST_TIMEOUT_MS, etc.), which this script had been missing. +// scripts/check-mcp-release-due.ts's GITHUB_REQUEST_TIMEOUT_MS, etc.), which this script had been missing. export const GITTENSOR_IMPACT_CARD_FETCH_TIMEOUT_MS = 10_000; const THEME = { cardBg: "#0e100d", @@ -30,12 +30,34 @@ const THEME = { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); -const repoIconPath = path.join( - repoRoot, - "apps/loopover-ui/public/brand/loopover-icon-citron.svg", -); +const repoIconPath = path.join(repoRoot, "apps/loopover-ui/public/brand/loopover-icon-citron.svg"); + +// Deliberately not `typeof fetch`: the global fetch type (in a Cloudflare Workers-typed environment) is +// overloaded to accept URL/RequestInfo/CfProperties, which a plainly-typed vi.fn() mock can't satisfy under +// strict function-type checking (see scripts/load-test-worker.ts's identical note). Both functions here +// only ever call fetchImpl with a string URL and a plain {headers?, signal} init. +export type ImpactCardFetch = ( + url: string, + init?: { headers?: Record; signal?: AbortSignal }, +) => Promise<{ ok?: boolean; status?: number; json?: () => Promise; text?: () => Promise }>; + +// The gittensor.io API's actual response shapes, scoped to only the fields this script reads -- neither +// endpoint's response is otherwise validated (matches the original .mjs's behavior of trusting the JSON as-is). +type GittensorImpact = { + totalPRs: number; + totalContributors: number; + totalLinesChanged: number; + emissionShare: number; +}; -function compact(n) { +type GittensorPr = { + mergedAt: string; + additions?: number; + deletions?: number; + author: string; +}; + +function compact(n: number): string { if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; if (n >= 1000) return (n / 1000).toFixed(0) + "k"; return String(n); @@ -43,7 +65,7 @@ function compact(n) { // api.gittensor.io values (and the repo name) end up as SVG content, // so escape XML special chars rather than trust they're clean numbers/strings. -function escapeXml(value) { +function escapeXml(value: unknown): string { return String(value) .replace(/&/g, "&") .replace(/ { const res = await fetchImpl(url, { headers: { "User-Agent": "gittensor-impact-card/1.0" }, signal: AbortSignal.timeout(GITTENSOR_IMPACT_CARD_FETCH_TIMEOUT_MS), }); if (!res.ok) throw new Error(`fetch failed: ${url} (${res.status})`); - return res.json(); + return res.json!(); } -export async function fetchGtLogoSvg(fetchImpl = fetch) { +export async function fetchGtLogoSvg(fetchImpl: ImpactCardFetch = fetch): Promise { const res = await fetchImpl("https://gittensor.io/gt-logo.svg", { signal: AbortSignal.timeout(GITTENSOR_IMPACT_CARD_FETCH_TIMEOUT_MS), }); - return res.text(); + return res.text!(); } -function bucketWeekly(prs, now) { +function bucketWeekly(prs: readonly GittensorPr[], now: Date) { const weekMs = 7 * 24 * 60 * 60 * 1000; const bucketStart = new Date(now.getTime() - WEEKS * weekMs); const prBuckets = Array(WEEKS).fill(0); const locBuckets = Array(WEEKS).fill(0); - const seenByBucket = Array.from({ length: WEEKS }, () => new Set()); + const seenByBucket: Set[] = Array.from({ length: WEEKS }, () => new Set()); const contributorBuckets = Array(WEEKS).fill(0); for (const pr of prs) { const t = new Date(pr.mergedAt); if (t < bucketStart || t > now) continue; - const idx = Math.min(WEEKS - 1, Math.floor((t - bucketStart) / weekMs)); + const idx = Math.min(WEEKS - 1, Math.floor((t.getTime() - bucketStart.getTime()) / weekMs)); prBuckets[idx] += 1; locBuckets[idx] += (pr.additions || 0) + (pr.deletions || 0); - seenByBucket[idx].add(pr.author); + seenByBucket[idx]!.add(pr.author); } - const seenSoFar = new Set(); + const seenSoFar = new Set(); for (let i = 0; i < WEEKS; i++) { - for (const a of seenByBucket[i]) seenSoFar.add(a); + for (const a of seenByBucket[i]!) seenSoFar.add(a); contributorBuckets[i] = seenSoFar.size; } return { prBuckets, locBuckets, contributorBuckets }; } -function sparkline(x, y, w, h, values, mutedColor, accentColor, cardBg) { +function sparkline(x: number, y: number, w: number, h: number, values: readonly number[], mutedColor: string, accentColor: string, cardBg: string): string { const max = Math.max(...values, 1); const min = Math.min(...values, 0); const range = max - min || 1; const n = values.length; const stepX = w / (n - 1); - const pts = values.map((v, i) => [ - x + i * stepX, - y + h - ((v - min) / range) * h, - ]); - const svgPath = pts - .map( - ([px, py], i) => - `${i === 0 ? "M" : "L"}${px.toFixed(1)},${py.toFixed(1)}`, - ) - .join(" "); - const [lastX, lastY] = pts[pts.length - 1]; + const pts = values.map((v, i): [number, number] => [x + i * stepX, y + h - ((v - min) / range) * h]); + const svgPath = pts.map(([px, py], i) => `${i === 0 ? "M" : "L"}${px!.toFixed(1)},${py!.toFixed(1)}`).join(" "); + const [lastX, lastY] = pts[pts.length - 1]!; return ` `; } -function meter(x, y, w, h, value, max, accentColor, trackColor) { +function meter(x: number, y: number, w: number, h: number, value: number, max: number, accentColor: string, trackColor: string): string { const fillW = Math.max(h, Math.min(value / max, 1) * w); return ` `; } -function render({ repo, impact, buckets, gtLogoB64, repoIconB64 }) { +function render({ + repo, + impact, + buckets, + gtLogoB64, + repoIconB64, +}: { + repo: string; + impact: GittensorImpact; + buckets: ReturnType; + gtLogoB64: string; + repoIconB64: string; +}): string { const { cardBg, fg, muted, accent, accentTrack, border, radius } = THEME; const W = 1200, H = 420; @@ -131,65 +157,25 @@ function render({ repo, impact, buckets, gtLogoB64, repoIconB64 }) { const cols = 4; const colW = (W - 2 * pad) / cols; const font = "'DM Sans', ui-sans-serif, system-ui, -apple-system, sans-serif"; - const displayFont = - "'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif"; + const displayFont = "'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif"; const sparkW = colW - 40; const sparkH = 56; const sparkY = 150; const stats = [ - { - type: "sparkline", - series: buckets.prBuckets, - value: impact.totalPRs.toLocaleString(), - label: "merged PRs", - }, - { - type: "sparkline", - series: buckets.contributorBuckets, - value: String(impact.totalContributors), - label: "contributors", - }, - { - type: "sparkline", - series: buckets.locBuckets, - value: compact(impact.totalLinesChanged), - label: "lines changed", - }, - { - type: "meter", - raw: impact.emissionShare * 100, - max: 100, - value: `${(impact.emissionShare * 100).toFixed(1)}%`, - label: "emission share", - }, + { type: "sparkline" as const, series: buckets.prBuckets, value: impact.totalPRs.toLocaleString(), label: "merged PRs" }, + { type: "sparkline" as const, series: buckets.contributorBuckets, value: String(impact.totalContributors), label: "contributors" }, + { type: "sparkline" as const, series: buckets.locBuckets, value: compact(impact.totalLinesChanged), label: "lines changed" }, + { type: "meter" as const, raw: impact.emissionShare * 100, max: 100, value: `${(impact.emissionShare * 100).toFixed(1)}%`, label: "emission share" }, ]; let statsSvg = ""; stats.forEach((s, i) => { const x = pad + i * colW; if (s.type === "meter") { - statsSvg += meter( - x, - sparkY + sparkH / 2 - 7, - sparkW, - 14, - s.raw, - s.max, - accent, - accentTrack, - ); + statsSvg += meter(x, sparkY + sparkH / 2 - 7, sparkW, 14, s.raw, s.max, accent, accentTrack); } else { - statsSvg += sparkline( - x, - sparkY, - sparkW, - sparkH, - s.series, - muted, - accent, - cardBg, - ); + statsSvg += sparkline(x, sparkY, sparkW, sparkH, s.series, muted, accent, cardBg); } statsSvg += ` ${escapeXml(s.value)} @@ -218,22 +204,18 @@ ${statsSvg} async function main() { const [repo, outFile] = process.argv.slice(2); if (!repo || !outFile) { - console.error( - "Usage: node scripts/gittensor-impact-card.mjs ", - ); + console.error("Usage: node scripts/gittensor-impact-card.ts "); process.exit(1); } const encoded = encodeURIComponent(repo); const [impact, prs, gtLogoSvg] = await Promise.all([ - fetchJson(`https://api.gittensor.io/repos/${encoded}/impact`), - fetchJson(`https://api.gittensor.io/repos/${encoded}/prs`), + fetchJson(`https://api.gittensor.io/repos/${encoded}/impact`) as Promise, + fetchJson(`https://api.gittensor.io/repos/${encoded}/prs`) as Promise, fetchGtLogoSvg(), ]); const buckets = bucketWeekly(prs, new Date()); const gtLogoB64 = Buffer.from(gtLogoSvg).toString("base64"); - const repoIconB64 = Buffer.from(readFileSync(repoIconPath)).toString( - "base64", - ); + const repoIconB64 = Buffer.from(readFileSync(repoIconPath)).toString("base64"); const svg = render({ repo, impact, buckets, gtLogoB64, repoIconB64 }); writeFileSync(outFile, svg); @@ -242,9 +224,8 @@ async function main() { // Run only when invoked directly as a CLI script, not when imported (e.g. by this file's own unit tests // for fetchJson/fetchGtLogoSvg) -- matches every other CLI script's entrypoint guard in this repo (e.g. -// scripts/check-mcp-release-due.mjs). -const entrypointPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; -if (import.meta.url === pathToFileURL(entrypointPath).href) { +// scripts/check-mcp-release-due.ts). +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { main().catch((err) => { console.error(err); process.exit(1); diff --git a/scripts/lint-composite-actions.d.mts b/scripts/lint-composite-actions.d.mts deleted file mode 100644 index 4d92ed6f60..0000000000 --- a/scripts/lint-composite-actions.d.mts +++ /dev/null @@ -1,31 +0,0 @@ -export type SchemaValidator = ((doc: unknown) => boolean) & { - errors?: Array<{ instancePath?: string; message?: string }> | null; -}; - -export type Dirent = { name: string; isDirectory(): boolean }; - -export type ReaddirFn = (dir: string, options: { withFileTypes: true }) => Dirent[]; - -export type ReadFileFn = (path: string, encoding?: string) => string | Uint8Array; - -export declare function compileActionSchema(schemaJson: unknown): SchemaValidator; - -export declare function findActionFiles( - actionsDir: string, - deps: { readdir: ReaddirFn; readFile: ReadFileFn }, -): string[]; - -export declare function validateActionFile( - path: string, - content: string, - validateSchema: SchemaValidator, -): string[]; - -export declare function runLint(options: { - actionsDir: string; - readdir: ReaddirFn; - readFile: ReadFileFn; - validateSchema: SchemaValidator; - log?: (message: string) => void; - error?: (message: string) => void; -}): number; diff --git a/scripts/lint-composite-actions.mjs b/scripts/lint-composite-actions.ts similarity index 75% rename from scripts/lint-composite-actions.mjs rename to scripts/lint-composite-actions.ts index ec08ded331..82197322ad 100644 --- a/scripts/lint-composite-actions.mjs +++ b/scripts/lint-composite-actions.ts @@ -22,28 +22,38 @@ import Ajv from "ajv"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL, URL } from "node:url"; import { parse } from "yaml"; const ACTIONS_DIR = ".github/actions"; -const SCHEMA_PATH = new URL("./schemas/github-action.schema.json", import.meta.url); +const SCHEMA_PATH = fileURLToPath(new URL("./schemas/github-action.schema.json", import.meta.url)); + +export type SchemaValidator = ((doc: unknown) => boolean) & { + errors?: Array<{ instancePath?: string; message?: string }> | null; +}; + +export type Dirent = { name: string; isDirectory(): boolean }; + +export type ReaddirFn = (dir: string, options: { withFileTypes: true }) => Dirent[]; + +export type ReadFileFn = (path: string, encoding?: string) => string | Uint8Array; /** Compile the vendored action-metadata schema into an Ajv validator. Exported (with the schema JSON * injectable) so tests validate against the real schema without this module reading it off disk. */ -export function compileActionSchema(schemaJson) { +export function compileActionSchema(schemaJson: unknown): SchemaValidator { const ajv = new Ajv({ allErrors: true, strict: false }); - return ajv.compile(schemaJson); + return ajv.compile(schemaJson as object) as unknown as SchemaValidator; } /** Discover action.yml/action.yaml files, one per subdirectory. `readdir`/`readFile` are injected so this * is testable without a real .github/actions tree; a subdirectory with neither file is silently skipped * (the per-candidate readFile throw is the existing fallback). */ -export function findActionFiles(actionsDir, { readdir, readFile }) { - const results = []; +export function findActionFiles(actionsDir: string, { readdir, readFile }: { readdir: ReaddirFn; readFile: ReadFileFn }): string[] { + const results: string[] = []; for (const entry of readdir(actionsDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; for (const name of ["action.yml", "action.yaml"]) { - const candidate = join(actionsDir, entry.name, name); + const candidate = join(actionsDir, entry.name, name!); try { readFile(candidate); results.push(candidate); @@ -58,8 +68,8 @@ export function findActionFiles(actionsDir, { readdir, readFile }) { /** Validate one action file's YAML text: JSON-schema violations, plus the composite-only rule that every * `run:` step carries an explicit `shell:`. Returns the human-readable error lines (empty === clean). */ -export function validateActionFile(path, content, validateSchema) { - const errors = []; +export function validateActionFile(path: string, content: string, validateSchema: SchemaValidator): string[] { + const errors: string[] = []; const doc = parse(content); if (!validateSchema(doc)) { @@ -84,7 +94,21 @@ export function validateActionFile(path, content, validateSchema) { /** Lint every discovered composite action; returns the process exit code (0 clean / 1 violations). Pure * aside from the injected `readdir`/`readFile`/`validateSchema` and the `log`/`error` sinks. */ -export function runLint({ actionsDir, readdir, readFile, validateSchema, log = console.log, error = console.error }) { +export function runLint({ + actionsDir, + readdir, + readFile, + validateSchema, + log = console.log, + error = console.error, +}: { + actionsDir: string; + readdir: ReaddirFn; + readFile: ReadFileFn; + validateSchema: SchemaValidator; + log?: (message: string) => void; + error?: (message: string) => void; +}): number { const actionFiles = findActionFiles(actionsDir, { readdir, readFile }); if (actionFiles.length === 0) { log(`No composite action files found under ${actionsDir}/ -- nothing to validate.`); @@ -93,7 +117,7 @@ export function runLint({ actionsDir, readdir, readFile, validateSchema, log = c let hasErrors = false; for (const path of actionFiles) { - const fileErrors = validateActionFile(path, readFile(path, "utf8"), validateSchema); + const fileErrors = validateActionFile(path, readFile(path, "utf8") as string, validateSchema); if (fileErrors.length > 0) { hasErrors = true; for (const line of fileErrors) error(line); @@ -111,8 +135,8 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { const validateSchema = compileActionSchema(JSON.parse(readFileSync(SCHEMA_PATH, "utf8"))); const exitCode = runLint({ actionsDir: ACTIONS_DIR, - readdir: readdirSync, - readFile: readFileSync, + readdir: readdirSync as unknown as ReaddirFn, + readFile: (path: string, encoding?: string) => readFileSync(path, encoding as BufferEncoding | undefined), validateSchema, }); process.exit(exitCode); diff --git a/scripts/load-test-worker.d.mts b/scripts/load-test-worker.d.mts deleted file mode 100644 index 7fe4e96318..0000000000 --- a/scripts/load-test-worker.d.mts +++ /dev/null @@ -1,61 +0,0 @@ -// Deliberately not `typeof fetch`: the global fetch type (in a Cloudflare Workers-typed environment) is -// overloaded to accept URL/RequestInfo/CfProperties, which a plain vi.fn() mock typed against a simple -// (url: string, init?) shape can't satisfy under strict function-type checking. load-test-worker.mjs only -// ever calls fetchImpl with a string URL and a plain {signal, headers} init, so this narrower shape is both -// what's actually used and what test mocks can trivially implement. -export type LoadTestFetch = ( - url: string, - init?: { signal?: AbortSignal; headers?: Record }, -) => Promise; - -export type RequestOnceOptions = { - fetchImpl?: LoadTestFetch; - timeoutMs?: number; -}; - -export type RequestOnceResult = { - ok: boolean; - status: number | null; - elapsedMs: number; - error?: string; -}; - -export type LoadTestOptions = { - origin?: string; - path?: string; - levels?: number[]; - requestCount?: number; - fetchImpl?: LoadTestFetch; - timeoutMs?: number; -}; - -export type LoadTestLevelResult = { - concurrency: number; - requestCount: number; - path: string; - wallMs: number; - successCount: number; - errorCount: number; - requestsPerSecond: number; - p50Ms: number; - p95Ms: number; -}; - -export declare const DEFAULT_ORIGIN: string; -export declare const DEFAULT_PATH: string; -export declare const DEFAULT_CONCURRENCY_LEVELS: number[]; -export declare const DEFAULT_REQUESTS_PER_LEVEL: number; -export declare const DEFAULT_TIMEOUT_MS: number; - -export declare function requestOnce(url: string, options?: RequestOnceOptions): Promise; - -export declare function percentile(values: readonly number[], p: number): number; - -export declare function runConcurrencyLevel( - concurrency: number, - options?: LoadTestOptions, -): Promise; - -export declare function runLoadTest(options?: LoadTestOptions): Promise; - -export declare function formatLoadTestReport(results: readonly LoadTestLevelResult[]): string; diff --git a/scripts/load-test-worker.mjs b/scripts/load-test-worker.ts similarity index 70% rename from scripts/load-test-worker.mjs rename to scripts/load-test-worker.ts index 41a75be551..f52aa1a1c0 100644 --- a/scripts/load-test-worker.mjs +++ b/scripts/load-test-worker.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { performance } from "node:perf_hooks"; +import { pathToFileURL } from "node:url"; // Load-testing harness for the Worker's key HTTP endpoints (#4913): every existing test under test/workers and // test/integration is correctness-oriented and drives the Worker in-process (worker.fetch(request, env, ctx) @@ -17,6 +18,46 @@ import { performance } from "node:perf_hooks"; // per level rather than treated as a hard script failure, since how a route degrades under load is itself part // of what this tool measures. +// Deliberately not `typeof fetch`: the global fetch type (in a Cloudflare Workers-typed environment) is +// overloaded to accept URL/RequestInfo/CfProperties, which a plain vi.fn() mock typed against a simple +// (url: string, init?) shape can't satisfy under strict function-type checking. requestOnce only ever calls +// fetchImpl with a string URL and a plain {signal, headers} init, so this narrower shape is both what's +// actually used and what test mocks can trivially implement. +export type LoadTestFetch = (url: string, init?: { signal?: AbortSignal; headers?: Record }) => Promise; + +export type RequestOnceOptions = { + fetchImpl?: LoadTestFetch; + timeoutMs?: number; +}; + +export type RequestOnceResult = { + ok: boolean; + status: number | null; + elapsedMs: number; + error?: string; +}; + +export type LoadTestOptions = { + origin?: string; + path?: string; + levels?: number[]; + requestCount?: number; + fetchImpl?: LoadTestFetch; + timeoutMs?: number; +}; + +export type LoadTestLevelResult = { + concurrency: number; + requestCount: number; + path: string; + wallMs: number; + successCount: number; + errorCount: number; + requestsPerSecond: number; + p50Ms: number; + p95Ms: number; +}; + export const DEFAULT_ORIGIN = "http://127.0.0.1:8787"; export const DEFAULT_PATH = "/health"; export const DEFAULT_CONCURRENCY_LEVELS = [1, 8, 32, 128]; @@ -26,7 +67,7 @@ export const DEFAULT_TIMEOUT_MS = 20_000; /** One GET request against `url`, using an injectable `fetchImpl` (defaults to global fetch) so tests can stub * the network entirely. Never throws: a connection error or an aborted timeout is reported in the same shape a * real HTTP response would be (`ok: false`, `status: null`), so one bad request never aborts its batch. */ -export async function requestOnce(url, options = {}) { +export async function requestOnce(url: string, options: RequestOnceOptions = {}): Promise { const fetchImpl = options.fetchImpl ?? fetch; const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const controller = new AbortController(); @@ -52,11 +93,11 @@ export async function requestOnce(url, options = {}) { /** The p-th percentile (0-100) of `values` via nearest-rank on a sorted copy. Returns 0 for an empty input so a * level with zero successful requests reports a well-defined (not NaN) latency. */ -export function percentile(values, p) { +export function percentile(values: readonly number[], p: number): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const rank = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1); - return sorted[Math.max(0, rank)]; + return sorted[Math.max(0, rank)]!; } /** @@ -65,19 +106,17 @@ export function percentile(values, p) { * and latency percentiles. Batching (rather than firing all `requestCount` requests in one `Promise.all`) keeps * each level's in-flight connection count bounded to `concurrency`, matching what "N concurrent users" means. */ -export async function runConcurrencyLevel(concurrency, options = {}) { +export async function runConcurrencyLevel(concurrency: number, options: LoadTestOptions = {}): Promise { const origin = options.origin ?? DEFAULT_ORIGIN; const path = options.path ?? DEFAULT_PATH; const requestCount = options.requestCount ?? DEFAULT_REQUESTS_PER_LEVEL; const url = `${origin}${path}`; const start = performance.now(); - const outcomes = []; + const outcomes: RequestOnceResult[] = []; for (let batchStart = 0; batchStart < requestCount; batchStart += concurrency) { const batchSize = Math.min(concurrency, requestCount - batchStart); - const batch = await Promise.all( - Array.from({ length: batchSize }, () => requestOnce(url, options)), - ); + const batch = await Promise.all(Array.from({ length: batchSize }, () => requestOnce(url, options))); outcomes.push(...batch); } const wallMs = performance.now() - start; @@ -99,9 +138,9 @@ export async function runConcurrencyLevel(concurrency, options = {}) { /** Run every concurrency level in `levels` in sequence (never overlapping each other), so one level's connection * contention never bleeds into the next level's measurement. */ -export async function runLoadTest(options = {}) { +export async function runLoadTest(options: LoadTestOptions = {}): Promise { const levels = options.levels ?? DEFAULT_CONCURRENCY_LEVELS; - const results = []; + const results: LoadTestLevelResult[] = []; for (const concurrency of levels) { results.push(await runConcurrencyLevel(concurrency, options)); } @@ -109,7 +148,7 @@ export async function runLoadTest(options = {}) { } /** Render load-test results as a stable, greppable text report (no locale-dependent number formatting). */ -export function formatLoadTestReport(results) { +export function formatLoadTestReport(results: readonly LoadTestLevelResult[]): string { const lines = ["worker load test", ""]; for (const r of results) { lines.push( @@ -121,8 +160,15 @@ export function formatLoadTestReport(results) { return lines.join("\n"); } -function parseArgs(argv) { - const args = { +type ParsedArgs = { + origin: string; + path: string; + levels: number[]; + requestCount: number; +}; + +function parseArgs(argv: readonly string[]): ParsedArgs { + const args: ParsedArgs = { origin: DEFAULT_ORIGIN, path: DEFAULT_PATH, levels: DEFAULT_CONCURRENCY_LEVELS, @@ -130,10 +176,10 @@ function parseArgs(argv) { }; for (let i = 0; i < argv.length; i += 1) { const flag = argv[i]; - if (flag === "--origin") args.origin = argv[++i]; - else if (flag === "--path") args.path = argv[++i]; + if (flag === "--origin") args.origin = argv[++i]!; + else if (flag === "--path") args.path = argv[++i]!; else if (flag === "--requests-per-level") args.requestCount = Number(argv[++i]); - else if (flag === "--levels") args.levels = argv[++i].split(",").map(Number); + else if (flag === "--levels") args.levels = argv[++i]!.split(",").map(Number); } return args; } @@ -149,6 +195,9 @@ async function main() { console.log(formatLoadTestReport(results)); } -if (import.meta.url === `file://${process.argv[1]}`) { - main(); +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); } diff --git a/scripts/mcp-package-allowlist.d.mts b/scripts/mcp-package-allowlist.d.mts deleted file mode 100644 index 27ad07ab14..0000000000 --- a/scripts/mcp-package-allowlist.d.mts +++ /dev/null @@ -1 +0,0 @@ -export const MCP_PACKAGE_ALLOWED_FILE_PATTERNS: RegExp[]; diff --git a/scripts/mcp-package-allowlist.mjs b/scripts/mcp-package-allowlist.ts similarity index 75% rename from scripts/mcp-package-allowlist.mjs rename to scripts/mcp-package-allowlist.ts index c0bf12162d..79cd455d6f 100644 --- a/scripts/mcp-package-allowlist.mjs +++ b/scripts/mcp-package-allowlist.ts @@ -1,8 +1,8 @@ // Canonical MCP published-tarball allowlist (#6291). Shared by check-mcp-package.mjs and -// mcp-release-candidate-core.mjs so the dry-run gate and the release-candidate tarball check +// mcp-release-candidate-core.ts so the dry-run gate and the release-candidate tarball check // cannot drift (the previous duplicated lists already missed shipped lib/*.js files). -export const MCP_PACKAGE_ALLOWED_FILE_PATTERNS = [ +export const MCP_PACKAGE_ALLOWED_FILE_PATTERNS: RegExp[] = [ /^bin\/loopover-mcp\.js$/, /^lib\/cli-error\.js$/, /^lib\/local-branch\.js$/, diff --git a/scripts/mcp-release-candidate-core.d.mts b/scripts/mcp-release-candidate-core.d.mts deleted file mode 100644 index a803928f45..0000000000 --- a/scripts/mcp-release-candidate-core.d.mts +++ /dev/null @@ -1,36 +0,0 @@ -export const RELEASE_TAG_PATTERN: RegExp; -export const MCP_PACKAGE_ALLOWED_FILE_PATTERNS: RegExp[]; - -export type CheckResult = { - ok: boolean; - code: string; - message: string; -}; - -export type TarballCheckResult = CheckResult & { - unexpected: string[]; - secretFiles: string[]; -}; - -export type TokenlessCheckResult = CheckResult & { - issues: string[]; -}; - -export type ReleaseCandidateReport = { - ok: boolean; - checks: Array<{ name: string; ok: boolean; code: string; message: string }>; - failures: Array<{ name: string; ok: boolean; code: string; message: string }>; - nextSteps: string[]; -}; - -export function parseReleaseTag(tag: string | null | undefined): { valid: boolean; version: string | null }; -export function expectedReleaseTag(version: string): string; -export function checkTag(input: { tag: string | null | undefined; packageVersion: string | null | undefined }): CheckResult; -export function changelogHasVersionSection(changelog: string | null | undefined, version: string | null | undefined): boolean; -export function checkChangelog(input: { changelog: string | null | undefined; version: string }): CheckResult; -export function unexpectedTarballFiles(files: string[] | null | undefined): string[]; -export function fileLooksLikeSecret(content: string | null | undefined): boolean; -export function checkTarball(input: { files: string[] | null | undefined; contentsByFile?: Record }): TarballCheckResult; -export function checkTokenlessPublish(workflowYaml: string | null | undefined): TokenlessCheckResult; -export function buildReleaseCandidateReport(checks: Record): ReleaseCandidateReport; -export function redactSensitive(text: string | null | undefined): string; diff --git a/scripts/mcp-release-candidate-core.mjs b/scripts/mcp-release-candidate-core.ts similarity index 66% rename from scripts/mcp-release-candidate-core.mjs rename to scripts/mcp-release-candidate-core.ts index 9549759759..037cb7e272 100644 --- a/scripts/mcp-release-candidate-core.mjs +++ b/scripts/mcp-release-candidate-core.ts @@ -1,6 +1,6 @@ -import { normalizeNewlines } from "./mcp-release-core.mjs"; -import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.mjs"; import YAML from "yaml"; +import { normalizeNewlines } from "./mcp-release-core.js"; +import { MCP_PACKAGE_ALLOWED_FILE_PATTERNS } from "./mcp-package-allowlist.js"; /** * Pure, deterministic checks for the MCP release-candidate dry-run. @@ -16,27 +16,49 @@ import YAML from "yaml"; export const RELEASE_TAG_PATTERN = /^mcp-v(\d+)\.(\d+)\.(\d+)$/; -// Canonical allowlist lives in mcp-package-allowlist.mjs (shared with check-mcp-package.mjs). +// Canonical allowlist lives in mcp-package-allowlist.ts (shared with check-mcp-package.mjs). export { MCP_PACKAGE_ALLOWED_FILE_PATTERNS }; const ALLOWED_FILE_PATTERNS = MCP_PACKAGE_ALLOWED_FILE_PATTERNS; const FORBIDDEN_PATH_PATTERN = /(^|\/)(\.dev\.vars|\.env|\.npmrc|.*\.pem|.*private.*key.*|.*secret.*)$/i; const SECRET_CONTENT_PATTERN = /(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+|gts_[0-9a-f]{64}|[A-Z0-9_]*(TOKEN|SECRET|PRIVATE_KEY)=)/; const NPM_TOKEN_PATTERN = /(NODE_AUTH_TOKEN|NPM_TOKEN|secrets\.NPM[A-Z_]*|_authToken|npm_[A-Za-z0-9]{20,})/; +export type CheckResult = { + ok: boolean; + code: string; + message: string; +}; + +export type TarballCheckResult = CheckResult & { + unexpected: string[]; + secretFiles: string[]; +}; + +export type TokenlessCheckResult = CheckResult & { + issues: string[]; +}; + +export type ReleaseCandidateReport = { + ok: boolean; + checks: Array<{ name: string; ok: boolean; code: string; message: string }>; + failures: Array<{ name: string; ok: boolean; code: string; message: string }>; + nextSteps: string[]; +}; + /** Parse a release tag, returning whether it is well-formed and its semver. */ -export function parseReleaseTag(tag) { +export function parseReleaseTag(tag: string | null | undefined): { valid: boolean; version: string | null } { const match = RELEASE_TAG_PATTERN.exec(String(tag ?? "").trim()); if (!match) return { valid: false, version: null }; return { valid: true, version: `${match[1]}.${match[2]}.${match[3]}` }; } /** The canonical tag for a package version. */ -export function expectedReleaseTag(version) { +export function expectedReleaseTag(version: string): string { return `mcp-v${version}`; } /** Verify the intended tag is well-formed and matches the package version. */ -export function checkTag({ tag, packageVersion }) { +export function checkTag({ tag, packageVersion }: { tag: string | null | undefined; packageVersion: string | null | undefined }): CheckResult { const parsed = parseReleaseTag(tag); if (!parsed.valid) { return { ok: false, code: "tag_format_invalid", message: `Release tag "${tag}" must be mcp-v.. (for example mcp-v${packageVersion ?? "0.0.0"}).` }; @@ -51,14 +73,14 @@ export function checkTag({ tag, packageVersion }) { } /** Whether the changelog contains a real, dated section for the target version. */ -export function changelogHasVersionSection(changelog, version) { +export function changelogHasVersionSection(changelog: string | null | undefined, version: string | null | undefined): boolean { if (!changelog || !version) return false; const pattern = new RegExp(`^## mcp-v${escapeRegExp(version)} - \\S`, "m"); return pattern.test(normalizeNewlines(changelog)); } /** Verify the MCP changelog has a target-version section. */ -export function checkChangelog({ changelog, version }) { +export function checkChangelog({ changelog, version }: { changelog: string | null | undefined; version: string }): CheckResult { if (changelogHasVersionSection(changelog, version)) { return { ok: true, code: "changelog_ok", message: `MCP changelog has a dated section for mcp-v${version}.` }; } @@ -66,26 +88,24 @@ export function checkChangelog({ changelog, version }) { } /** Files that fall outside the publish allowlist (unexpected or forbidden). */ -export function unexpectedTarballFiles(files) { - return (files ?? []) - .map((file) => String(file)) - .filter((file) => FORBIDDEN_PATH_PATTERN.test(file) || !ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file))); +export function unexpectedTarballFiles(files: string[] | null | undefined): string[] { + return (files ?? []).map((file) => String(file)).filter((file) => FORBIDDEN_PATH_PATTERN.test(file) || !ALLOWED_FILE_PATTERNS.some((pattern) => pattern.test(file))); } /** Whether a file's content carries secret-like material. */ -export function fileLooksLikeSecret(content) { +export function fileLooksLikeSecret(content: string | null | undefined): boolean { return SECRET_CONTENT_PATTERN.test(String(content ?? "")); } /** Verify the packed tarball only contains allowlisted files with no secret-like content. */ -export function checkTarball({ files, contentsByFile }) { +export function checkTarball({ files, contentsByFile }: { files: string[] | null | undefined; contentsByFile?: Record }): TarballCheckResult { const unexpected = unexpectedTarballFiles(files); const secretFiles = Object.entries(contentsByFile ?? {}) .filter(([, content]) => fileLooksLikeSecret(content)) .map(([file]) => file) .sort(); const ok = unexpected.length === 0 && secretFiles.length === 0; - const problems = []; + const problems: string[] = []; if (unexpected.length > 0) problems.push(`unexpected file(s): ${unexpected.join(", ")}`); if (secretFiles.length > 0) problems.push(`secret-like content in: ${secretFiles.join(", ")}`); return { @@ -100,9 +120,9 @@ export function checkTarball({ files, contentsByFile }) { } /** Verify the publish workflow uses tokenless trusted publishing (OIDC + provenance, no npm token). */ -export function checkTokenlessPublish(workflowYaml) { +export function checkTokenlessPublish(workflowYaml: string | null | undefined): TokenlessCheckResult { const yaml = String(workflowYaml ?? ""); - const issues = []; + const issues: string[] = []; const workflow = parseWorkflowYaml(yaml); const publishJobs = findPublishJobs(workflow); if (publishJobs.length === 0) { @@ -120,13 +140,14 @@ export function checkTokenlessPublish(workflowYaml) { ok, code: ok ? "publish_tokenless" : "publish_token_risk", issues, - message: ok - ? "Publish workflow uses tokenless trusted publishing (id-token + provenance, no npm token)." - : `Publish workflow provenance/tokenless config needs attention — ${issues.join("; ")}.`, + message: ok ? "Publish workflow uses tokenless trusted publishing (id-token + provenance, no npm token)." : `Publish workflow provenance/tokenless config needs attention — ${issues.join("; ")}.`, }; } -function parseWorkflowYaml(yaml) { +// The workflow YAML this parses is untyped external config (an arbitrary GitHub Actions workflow file) that +// this module itself only ever probes loosely (typeof/Array.isArray checks, never a full schema) -- typing it +// `any` here matches what the code actually relies on rather than forcing a schema this file doesn't validate. +function parseWorkflowYaml(yaml: string): any { try { const parsed = YAML.parse(yaml); return parsed && typeof parsed === "object" ? parsed : {}; @@ -135,30 +156,30 @@ function parseWorkflowYaml(yaml) { } } -function findPublishJobs(workflow) { +function findPublishJobs(workflow: any): Array<{ job: any; publishRuns: string[] }> { return Object.values(workflow?.jobs ?? {}) - .filter((job) => job && typeof job === "object") - .map((job) => ({ + .filter((job: any) => job && typeof job === "object") + .map((job: any) => ({ job, publishRuns: (Array.isArray(job.steps) ? job.steps : []) - .map((step) => (step && typeof step === "object" ? String(step.run ?? "") : "")) - .filter((run) => /\bnpm(?:@[^\s]+)?\s+publish\b/.test(run)), + .map((step: any) => (step && typeof step === "object" ? String(step.run ?? "") : "")) + .filter((run: string) => /\bnpm(?:@[^\s]+)?\s+publish\b/.test(run)), })) - .filter((job) => job.publishRuns.length > 0); + .filter((job: any) => job.publishRuns.length > 0); } -function hasIdTokenWrite(permissions) { +function hasIdTokenWrite(permissions: any): boolean { return permissions && typeof permissions === "object" && String(permissions["id-token"] ?? "").toLowerCase() === "write"; } -function hasEnabledProvenanceFlag(run) { +function hasEnabledProvenanceFlag(run: string): boolean { return run .split(/\r?\n/) .map((line) => line.replace(/(^|\s)#.*$/, "")) .some((line) => /(^|\s)--provenance(?:\s|$)/.test(line)); } -const REMEDIATION = { +const REMEDIATION: Record = { tag_format_invalid: "Use an mcp-v.. tag that matches the package version.", package_version_missing: "Restore a valid version in packages/loopover-mcp/package.json.", tag_version_mismatch: "Align the tag with packages/loopover-mcp/package.json (and the CLI packageVersion) before tagging.", @@ -169,27 +190,20 @@ const REMEDIATION = { }; /** Aggregate individual check results into a pass/fail report with next steps. */ -export function buildReleaseCandidateReport(checks) { +export function buildReleaseCandidateReport(checks: Record): ReleaseCandidateReport { const entries = Object.entries(checks) - .filter(([, result]) => result && typeof result === "object") + .filter((entry): entry is [string, CheckResult & { tag?: string }] => Boolean(entry[1]) && typeof entry[1] === "object") .map(([name, result]) => ({ name, ok: Boolean(result.ok), code: result.code, message: result.message })); const failures = entries.filter((entry) => !entry.ok); const ok = failures.length === 0; const nextSteps = ok - ? [ - "Release candidate looks safe to tag.", - `Create and push ${checks.tag?.tag ?? "the mcp-v tag"} to start the tokenless publish workflow.`, - "No publish was attempted by this dry-run.", - ] - : [ - ...failures.map((failure) => REMEDIATION[failure.code] ?? `Resolve: ${failure.message}`), - "Re-run the release-candidate dry-run; do not tag until it passes.", - ]; + ? ["Release candidate looks safe to tag.", `Create and push ${checks.tag?.tag ?? "the mcp-v tag"} to start the tokenless publish workflow.`, "No publish was attempted by this dry-run."] + : [...failures.map((failure) => REMEDIATION[failure.code] ?? `Resolve: ${failure.message}`), "Re-run the release-candidate dry-run; do not tag until it passes."]; return { ok, checks: entries, failures, nextSteps }; } /** Scrub tokens, npm credentials, GitHub auth, and absolute local paths from any log line. */ -export function redactSensitive(text) { +export function redactSensitive(text: string | null | undefined): string { return String(text ?? "") .replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[redacted-token]") .replace(/github_pat_[A-Za-z0-9_]+/g, "[redacted-token]") @@ -200,6 +214,6 @@ export function redactSensitive(text) { .replace(/(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)[^\s"';]*/g, "[local-path]"); } -function escapeRegExp(value) { +function escapeRegExp(value: string): string { return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/scripts/mcp-release-core.d.mts b/scripts/mcp-release-core.d.mts deleted file mode 100644 index c129a0122d..0000000000 --- a/scripts/mcp-release-core.d.mts +++ /dev/null @@ -1,43 +0,0 @@ -export const MCP_RELEASE_DUE_MARKER: string; - -export type McpReleaseCommit = { - sha?: string; - subject?: string; - body?: string; - files?: string[]; -}; - -export type McpReleaseReport = { - due: boolean; - proposedVersion: string; - latestTag: string | null; - latestTagVersion: string | null; - packageVersion: string; - publishedVersion: string | null; - releaseType: "major" | "minor" | "patch" | null; - commits: McpReleaseCommit[]; - changedFiles: string[]; -}; - -export function parseConventionalSubject(subject: string): { - type: string | null; - scope: string | null; - breaking: boolean; - description: string; - conventional: boolean; -}; -export function compareSemver(leftVersion: string, rightVersion: string): number | null; -export function bumpVersion(version: string, releaseType: "major" | "minor" | "patch"): string; -export function latestSemverTag(tags: string[]): { tag: string; version: string } | null; -export function selectMcpReleaseCommits(commits: T[]): T[]; -export function isMcpReleaseRelevantCommit(commit: McpReleaseCommit): boolean; -export function renderMcpChangelog(input: { existingChangelog?: string; targetVersion: string; generatedAt: string; commits: McpReleaseCommit[] }): string; -export function renderReleaseSection(input: { tag: string; generatedAt: string; commits: McpReleaseCommit[] }): string; -export function buildMcpReleaseReport(input: { - latestTag: { tag: string; version: string } | null; - packageVersion: string; - publishedVersion: string | null; - commits: McpReleaseCommit[]; -}): McpReleaseReport; -export function buildMcpReleaseIssue(report: McpReleaseReport): { title: string; body: string }; -export function normalizeNewlines(value: string): string; diff --git a/scripts/mcp-release-core.mjs b/scripts/mcp-release-core.ts similarity index 74% rename from scripts/mcp-release-core.mjs rename to scripts/mcp-release-core.ts index b9f8a6c76d..0c8c221ff7 100644 --- a/scripts/mcp-release-core.mjs +++ b/scripts/mcp-release-core.ts @@ -29,15 +29,42 @@ const RELEASE_SCOPES = new Set(["release", "changelog"]); const EXCLUDED_SCOPES = new Set(["pwa", "ui", "extension", "github-agent", "sync", "upstream"]); const GROUP_ORDER = ["Features", "Fixes", "Security", "CI", "Build", "Docs", "Tests", "Refactors", "Dependencies", "Chores", "Reverts"]; -export function parseConventionalSubject(subject) { +export type McpReleaseCommit = { + sha?: string; + subject?: string; + body?: string; + files?: string[]; +}; + +export type McpReleaseReport = { + due: boolean; + proposedVersion: string; + latestTag: string | null; + latestTagVersion: string | null; + packageVersion: string; + publishedVersion: string | null; + releaseType: "major" | "minor" | "patch" | null; + commits: McpReleaseCommit[]; + changedFiles: string[]; +}; + +type ParsedConventionalSubject = { + type: string | null; + scope: string | null; + breaking: boolean; + description: string; + conventional: boolean; +}; + +export function parseConventionalSubject(subject: string): ParsedConventionalSubject { const trimmed = subject.trim(); const match = /^(?[a-z]+)(?:\((?[^)]+)\))?(?!)?:\s*(?.+)$/.exec(trimmed); if (match?.groups) { return { - type: match.groups.type, + type: match.groups.type!, scope: match.groups.scope ?? null, breaking: Boolean(match.groups.breaking), - description: match.groups.description.trim(), + description: match.groups.description!.trim(), conventional: true, }; } @@ -61,11 +88,18 @@ export function parseConventionalSubject(subject) { }; } -export function compareSemver(leftVersion, rightVersion) { +type ParsedSemver = { + major: number; + minor: number; + patch: number; + prerelease: string | null; +}; + +export function compareSemver(leftVersion: string, rightVersion: string): number | null { const left = parseSemver(leftVersion); const right = parseSemver(rightVersion); if (!left || !right) return null; - for (const part of ["major", "minor", "patch"]) { + for (const part of ["major", "minor", "patch"] as const) { if (left[part] !== right[part]) return left[part] < right[part] ? -1 : 1; } if (left.prerelease === right.prerelease) return 0; @@ -75,7 +109,7 @@ export function compareSemver(leftVersion, rightVersion) { return prereleaseComparison === 0 ? 0 : prereleaseComparison < 0 ? -1 : 1; } -export function bumpVersion(version, releaseType) { +export function bumpVersion(version: string, releaseType: "major" | "minor" | "patch"): string { const parsed = parseSemver(version); if (!parsed) throw new Error(`Invalid semver version: ${version}`); if (releaseType === "major") return `${parsed.major + 1}.0.0`; @@ -83,18 +117,20 @@ export function bumpVersion(version, releaseType) { return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`; } -export function latestSemverTag(tags) { - return tags - .map((tag) => ({ tag, version: tag.replace(/^mcp-v/, "") })) - .filter(({ version }) => parseSemver(version)) - .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null; +export function latestSemverTag(tags: readonly string[]): { tag: string; version: string } | null { + return ( + tags + .map((tag) => ({ tag, version: tag.replace(/^mcp-v/, "") })) + .filter(({ version }) => parseSemver(version)) + .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null + ); } -export function selectMcpReleaseCommits(commits) { +export function selectMcpReleaseCommits(commits: readonly T[]): T[] { return commits.filter((commit) => isMcpReleaseRelevantCommit(commit)); } -export function isMcpReleaseRelevantCommit(commit) { +export function isMcpReleaseRelevantCommit(commit: McpReleaseCommit): boolean { const subject = commit.subject ?? ""; const files = uniqueSorted(commit.files ?? []); if (!subject.trim()) return false; @@ -120,7 +156,17 @@ export function isMcpReleaseRelevantCommit(commit) { return false; } -export function renderMcpChangelog({ existingChangelog = "", targetVersion, generatedAt, commits }) { +export function renderMcpChangelog({ + existingChangelog = "", + targetVersion, + generatedAt, + commits, +}: { + existingChangelog?: string; + targetVersion: string; + generatedAt: string; + commits: McpReleaseCommit[]; +}): string { const targetTag = `mcp-v${targetVersion}`; const normalizedExisting = normalizeNewlines(existingChangelog).trimEnd(); const headerMatch = /^# Changelog\n+/.exec(normalizedExisting); @@ -140,7 +186,7 @@ export function renderMcpChangelog({ existingChangelog = "", targetVersion, gene return historical ? `${header}${newSection}\n\n${historical}` : `${header}${newSection}\n`; } -export function renderReleaseSection({ tag, generatedAt, commits }) { +export function renderReleaseSection({ tag, generatedAt, commits }: { tag: string; generatedAt: string; commits: McpReleaseCommit[] }): string { const groups = groupCommits(commits); const lines = [`## ${tag} - ${generatedAt}`]; for (const group of GROUP_ORDER) { @@ -155,15 +201,22 @@ export function renderReleaseSection({ tag, generatedAt, commits }) { return lines.join("\n"); } -export function buildMcpReleaseReport({ latestTag, packageVersion, publishedVersion, commits }) { +export function buildMcpReleaseReport({ + latestTag, + packageVersion, + publishedVersion, + commits, +}: { + latestTag: { tag: string; version: string } | null; + packageVersion: string; + publishedVersion: string | null; + commits: McpReleaseCommit[]; +}): McpReleaseReport { const includedCommits = selectMcpReleaseCommits(commits); const releaseType = inferReleaseType(includedCommits); const latestVersion = latestTag?.version ?? "0.0.0"; const inferredVersion = releaseType ? bumpVersion(latestVersion, releaseType) : latestVersion; - const proposedVersion = - packageVersion && compareSemver(packageVersion, inferredVersion) === 1 - ? packageVersion - : inferredVersion; + const proposedVersion = packageVersion && compareSemver(packageVersion, inferredVersion) === 1 ? packageVersion : inferredVersion; const tagMatchesPackage = latestTag?.version === packageVersion; const npmMatchesPackage = publishedVersion === packageVersion; const due = includedCommits.length > 0 || !tagMatchesPackage || !npmMatchesPackage; @@ -181,7 +234,7 @@ export function buildMcpReleaseReport({ latestTag, packageVersion, publishedVers }; } -export function buildMcpReleaseIssue(report) { +export function buildMcpReleaseIssue(report: McpReleaseReport): { title: string; body: string } { const title = `MCP release due: ${report.proposedVersion}`; const npmVersion = report.publishedVersion ?? "unknown"; const latestTag = report.latestTag ?? "none"; @@ -230,18 +283,18 @@ ${changedFiles} return { title, body }; } -function escapeIssueMarkdownText(value) { +function escapeIssueMarkdownText(value: string | undefined): string { return String(value ?? "") - .replace(/[\u0000-\u001f\u007f]/g, " ") + .replace(/[\x00-\x1f\x7f]/g, " ") .replace(/@/g, "@\u200b") .replace(/([\\`*_{}[\]()#+.!|>-])/g, "\\$1"); } -export function normalizeNewlines(value) { +export function normalizeNewlines(value: string): string { return value.replace(/\r\n/g, "\n"); } -function parseSemver(version) { +function parseSemver(version: string | null | undefined): ParsedSemver | null { const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(version ?? "").trim()); if (!match) return null; return { @@ -252,37 +305,37 @@ function parseSemver(version) { }; } -function isClientVisibleChange(parsed, subject) { +function isClientVisibleChange(parsed: ParsedConventionalSubject, subject: string): boolean { if (parsed.breaking) return true; if (parsed.type === "feat" || parsed.type === "fix" || parsed.type === "refactor") return true; if (parsed.type === "docs" && /mcp|local|branch|compat|client|release/i.test(subject)) return true; return /^fix\b/i.test(subject); } -function isGeneratedOnlyOpenApiChange(files) { +function isGeneratedOnlyOpenApiChange(files: readonly string[]): boolean { const meaningfulFiles = files.filter((file) => !isTestFile(file)); return meaningfulFiles.length > 0 && meaningfulFiles.every((file) => GENERATED_OPENAPI_PATHS.includes(file)); } -function isUiOnlyChange(files) { +function isUiOnlyChange(files: readonly string[]): boolean { const meaningfulFiles = files.filter((file) => !isTestFile(file)); return meaningfulFiles.length > 0 && meaningfulFiles.every((file) => UI_ONLY_PREFIXES.some((prefix) => file.startsWith(prefix))); } -function hasAnyPath(files, paths) { +function hasAnyPath(files: readonly string[], paths: readonly string[]): boolean { return files.some((file) => paths.some((path) => matchesPath(file, path))); } -function matchesPath(file, path) { +function matchesPath(file: string, path: string): boolean { return path.endsWith("/") ? file.startsWith(path) : file === path; } -function isTestFile(file) { +function isTestFile(file: string): boolean { return file.startsWith("test/") || file.includes(".test.") || file.includes(".spec."); } -function groupCommits(commits) { - const groups = new Map(GROUP_ORDER.map((group) => [group, []])); +function groupCommits(commits: readonly McpReleaseCommit[]): Map { + const groups = new Map(GROUP_ORDER.map((group) => [group, []])); for (const commit of commits) { const group = groupForCommit(commit); groups.get(group)?.push(commit); @@ -290,7 +343,7 @@ function groupCommits(commits) { return groups; } -function groupForCommit(commit) { +function groupForCommit(commit: McpReleaseCommit): string { const parsed = parseConventionalSubject(commit.subject ?? ""); if (parsed.scope === "security") return "Security"; if (parsed.type === "feat") return "Features"; @@ -306,15 +359,15 @@ function groupForCommit(commit) { return "Chores"; } -function formatCommitForChangelog(commit) { +function formatCommitForChangelog(commit: McpReleaseCommit): string { const parsed = parseConventionalSubject(commit.subject ?? ""); const description = parsed.description || commit.subject || shortSha(commit.sha); return upperFirst(description); } -function inferReleaseType(commits) { +function inferReleaseType(commits: readonly McpReleaseCommit[]): "major" | "minor" | "patch" | null { if (commits.length === 0) return null; - let type = "patch"; + let type: "major" | "minor" | "patch" = "patch"; for (const commit of commits) { const parsed = parseConventionalSubject(commit.subject ?? ""); if (parsed.breaking || /BREAKING CHANGE:/i.test(commit.body ?? "")) return "major"; @@ -323,19 +376,19 @@ function inferReleaseType(commits) { return type; } -function uniqueSorted(values) { +function uniqueSorted(values: readonly string[]): string[] { return [...new Set(values.filter(Boolean))].sort((left, right) => left.localeCompare(right)); } -function shortSha(sha) { +function shortSha(sha: string | undefined): string { return String(sha ?? "").slice(0, 7); } -function upperFirst(value) { +function upperFirst(value: string): string { const trimmed = value.trim(); - return trimmed ? `${trimmed[0].toUpperCase()}${trimmed.slice(1)}` : trimmed; + return trimmed ? `${trimmed[0]!.toUpperCase()}${trimmed.slice(1)}` : trimmed; } -function escapeRegExp(value) { +function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/scripts/orb-release-core.d.mts b/scripts/orb-release-core.d.mts deleted file mode 100644 index 9b10164718..0000000000 --- a/scripts/orb-release-core.d.mts +++ /dev/null @@ -1,62 +0,0 @@ -export type OrbReleaseCommit = { - sha?: string; - subject?: string; - body?: string; - files?: string[]; -}; - -export type OrbSemver = { - major: number; - minor: number; - patch: number; - prerelease: string | null; -}; - -export type OrbBetaSemver = OrbSemver & { - betaNumber: number | null; -}; - -export type OrbReleaseReport = { - due: boolean; - targetVersion: string; - nextTag: string; - manifestVersion: string | null; - manifestStale: boolean; - inferredVersion: string; - latestStableTag: string | null; - latestTag: string | null; - commits: OrbReleaseCommit[]; - commitsSinceStable: OrbReleaseCommit[]; -}; - -export type OrbStableReleaseReport = { - due: boolean; - stableVersion: string; - nextVersion: string; - releaseType: "major" | "minor" | "patch" | null; - latestStableTag: string | null; - commits: OrbReleaseCommit[]; -}; - -export function parseConventionalSubject(subject: string): { - type: string | null; - scope: string | null; - breaking: boolean; - description: string; - conventional: boolean; -}; -export function parseSemver(version: string): OrbSemver | null; -export function parseOrbBetaVersion(version: string): OrbBetaSemver | null; -export function compareSemver(leftVersion: string, rightVersion: string): number | null; -export function bumpVersion(version: string, releaseType: "major" | "minor" | "patch"): string; -export function latestStableOrbTag(tags: string[]): { tag: string; version: string } | null; -export function latestOrbTag(tags: string[]): { tag: string; version: string } | null; -export function isImageRelevantCommit(commit: OrbReleaseCommit): boolean; -export function selectImageRelevantCommits(commits: T[]): T[]; -export function inferReleaseType(commits: OrbReleaseCommit[]): "major" | "minor" | "patch" | null; -export function buildOrbReleaseReport(input: { - tags: string[]; - manifestVersion: string | null; - commits: { sinceStable: OrbReleaseCommit[]; sinceLastTag: OrbReleaseCommit[] }; -}): OrbReleaseReport; -export function buildOrbStableReleaseReport(input: { tags: string[]; commitsSinceStable?: OrbReleaseCommit[] }): OrbStableReleaseReport; diff --git a/scripts/orb-release-core.mjs b/scripts/orb-release-core.ts similarity index 71% rename from scripts/orb-release-core.mjs rename to scripts/orb-release-core.ts index 2223733600..19d4a4fb4f 100644 --- a/scripts/orb-release-core.mjs +++ b/scripts/orb-release-core.ts @@ -1,5 +1,5 @@ // Pure logic for the ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost) automated beta -// channel. Deliberately independent of scripts/mcp-release-core.mjs (no shared imports/state) even though the +// channel. Deliberately independent of scripts/mcp-release-core.ts (no shared imports/state) even though the // shape mirrors it closely -- ORB and the MCP package are versioned, tagged, and published on separate // schedules by separate automation, and keeping them decoupled means neither can accidentally regress the // other's release path. @@ -25,8 +25,8 @@ const IMAGE_RELEVANT_PREFIXES = [ "scripts/deploy-selfhost-prebuilt.sh", "scripts/lib/selfhost-deploy-common.sh", "scripts/selfhost-post-update-check.sh", - "scripts/validate-selfhost-sourcemap.mjs", - "scripts/gen-selfhost-env-reference.mjs", + "scripts/validate-selfhost-sourcemap.ts", + "scripts/gen-selfhost-env-reference.ts", "scripts/export-grafana-reporting-db.sh", ".github/workflows/release-selfhost.yml", ]; @@ -42,22 +42,70 @@ const EXCLUDED_PREFIXES = [ "src/env.d.ts", // ambient Worker binding types only -- never reachable at self-host runtime ]; -export function parseConventionalSubject(subject) { +export type OrbReleaseCommit = { + sha?: string; + subject?: string; + body?: string; + files?: string[]; +}; + +export type OrbSemver = { + major: number; + minor: number; + patch: number; + prerelease: string | null; +}; + +export type OrbBetaSemver = OrbSemver & { + betaNumber: number | null; +}; + +export type OrbReleaseReport = { + due: boolean; + targetVersion: string; + nextTag: string; + manifestVersion: string | null; + manifestStale: boolean; + inferredVersion: string; + latestStableTag: string | null; + latestTag: string | null; + commits: OrbReleaseCommit[]; + commitsSinceStable: OrbReleaseCommit[]; +}; + +export type OrbStableReleaseReport = { + due: boolean; + stableVersion: string; + nextVersion: string; + releaseType: "major" | "minor" | "patch" | null; + latestStableTag: string | null; + commits: OrbReleaseCommit[]; +}; + +type ParsedConventionalSubject = { + type: string | null; + scope: string | null; + breaking: boolean; + description: string; + conventional: boolean; +}; + +export function parseConventionalSubject(subject: string): ParsedConventionalSubject { const trimmed = subject.trim(); const match = /^(?[a-z]+)(?:\((?[^)]+)\))?(?!)?:\s*(?.+)$/.exec(trimmed); if (match?.groups) { return { - type: match.groups.type, + type: match.groups.type!, scope: match.groups.scope ?? null, breaking: Boolean(match.groups.breaking), - description: match.groups.description.trim(), + description: match.groups.description!.trim(), conventional: true, }; } return { type: null, scope: null, breaking: false, description: trimmed, conventional: false }; } -export function parseSemver(version) { +export function parseSemver(version: string | null | undefined): OrbSemver | null { const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(version ?? "").trim()); if (!match) return null; return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] ?? null }; @@ -66,18 +114,18 @@ export function parseSemver(version) { /** Parses ONLY the beta-channel shape this repo actually uses (`X.Y.Z-beta.N`) -- any other prerelease label * (an `-rc.N`, a bare stable tag, or an unrecognized suffix) returns `betaNumber: null`, since it isn't a * beta-channel tag this module's counter logic applies to. */ -export function parseOrbBetaVersion(version) { +export function parseOrbBetaVersion(version: string): OrbBetaSemver | null { const parsed = parseSemver(version); if (!parsed) return null; const betaMatch = /^beta\.(\d+)$/.exec(parsed.prerelease ?? ""); return { ...parsed, betaNumber: betaMatch ? Number(betaMatch[1]) : null }; } -export function compareSemver(leftVersion, rightVersion) { +export function compareSemver(leftVersion: string, rightVersion: string): number | null { const left = parseSemver(leftVersion); const right = parseSemver(rightVersion); if (!left || !right) return null; - for (const part of ["major", "minor", "patch"]) { + for (const part of ["major", "minor", "patch"] as const) { if (left[part] !== right[part]) return left[part] < right[part] ? -1 : 1; } if (left.prerelease === right.prerelease) return 0; @@ -87,7 +135,7 @@ export function compareSemver(leftVersion, rightVersion) { return comparison === 0 ? 0 : comparison < 0 ? -1 : 1; } -export function bumpVersion(version, releaseType) { +export function bumpVersion(version: string, releaseType: "major" | "minor" | "patch"): string { const parsed = parseSemver(version); if (!parsed) throw new Error(`Invalid semver version: ${version}`); if (releaseType === "major") return `${parsed.major + 1}.0.0`; @@ -97,23 +145,27 @@ export function bumpVersion(version, releaseType) { /** The highest STABLE (no prerelease suffix) orb-v tag -- the baseline every commit-scan/bump calculation * measures from, regardless of how many beta snapshots have been cut since. */ -export function latestStableOrbTag(tags) { - return tags - .map((tag) => ({ tag, version: tag.startsWith(ORB_TAG_PREFIX) ? tag.slice(ORB_TAG_PREFIX.length) : null })) - .filter(({ version }) => version && parseSemver(version) !== null && parseSemver(version).prerelease === null) - .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null; +export function latestStableOrbTag(tags: readonly string[]): { tag: string; version: string } | null { + return ( + tags + .map((tag) => ({ tag, version: tag.startsWith(ORB_TAG_PREFIX) ? tag.slice(ORB_TAG_PREFIX.length) : null })) + .filter((entry): entry is { tag: string; version: string } => entry.version !== null && parseSemver(entry.version) !== null && parseSemver(entry.version)!.prerelease === null) + .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null + ); } /** The highest orb-v tag of ANY kind (stable or beta) -- where the next commit-scan window starts, and what * the next beta number counts up from when the target version hasn't changed. */ -export function latestOrbTag(tags) { - return tags - .map((tag) => ({ tag, version: tag.startsWith(ORB_TAG_PREFIX) ? tag.slice(ORB_TAG_PREFIX.length) : null })) - .filter(({ version }) => version && parseSemver(version)) - .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null; +export function latestOrbTag(tags: readonly string[]): { tag: string; version: string } | null { + return ( + tags + .map((tag) => ({ tag, version: tag.startsWith(ORB_TAG_PREFIX) ? tag.slice(ORB_TAG_PREFIX.length) : null })) + .filter((entry): entry is { tag: string; version: string } => entry.version !== null && parseSemver(entry.version) !== null) + .sort((left, right) => compareSemver(right.version, left.version) ?? 0)[0] ?? null + ); } -export function isImageRelevantCommit(commit) { +export function isImageRelevantCommit(commit: OrbReleaseCommit): boolean { const subject = (commit.subject ?? "").trim(); if (!subject) return false; if (/^merge\b/i.test(subject)) return false; @@ -123,17 +175,17 @@ export function isImageRelevantCommit(commit) { return relevantFiles.length > 0; } -export function selectImageRelevantCommits(commits) { +export function selectImageRelevantCommits(commits: readonly T[]): T[] { return commits.filter((commit) => isImageRelevantCommit(commit)); } -function matchesAnyPrefix(file, prefixes) { +function matchesAnyPrefix(file: string, prefixes: readonly string[]): boolean { return prefixes.some((prefix) => (prefix.endsWith("/") ? file.startsWith(prefix) : file === prefix)); } -export function inferReleaseType(commits) { +export function inferReleaseType(commits: readonly OrbReleaseCommit[]): "major" | "minor" | "patch" | null { if (commits.length === 0) return null; - let type = "patch"; + let type: "major" | "minor" | "patch" = "patch"; for (const commit of commits) { const parsed = parseConventionalSubject(commit.subject ?? ""); if (parsed.breaking || /BREAKING CHANGE:/i.test(commit.body ?? "")) return "major"; @@ -148,7 +200,15 @@ export function inferReleaseType(commits) { * FLAGS that the commits since the last stable tag imply a bigger bump than the manifest currently declares * (e.g. a `feat:` landed but the manifest still says a patch-level target), for a human to act on. */ -export function buildOrbReleaseReport({ tags, manifestVersion, commits }) { +export function buildOrbReleaseReport({ + tags, + manifestVersion, + commits, +}: { + tags: readonly string[]; + manifestVersion: string | null; + commits: { sinceStable: OrbReleaseCommit[]; sinceLastTag: OrbReleaseCommit[] }; +}): OrbReleaseReport { const stableTag = latestStableOrbTag(tags); const anyTag = latestOrbTag(tags); const stableVersion = stableTag?.version ?? "0.0.0"; @@ -156,7 +216,7 @@ export function buildOrbReleaseReport({ tags, manifestVersion, commits }) { const commitsSinceStable = selectImageRelevantCommits(commits.sinceStable ?? []); const inferredReleaseType = inferReleaseType(commitsSinceStable); const inferredVersion = inferredReleaseType ? bumpVersion(stableVersion, inferredReleaseType) : stableVersion; - const manifestStale = Boolean(manifestVersion) && compareSemver(inferredVersion, manifestVersion) === 1; + const manifestStale = Boolean(manifestVersion) && compareSemver(inferredVersion, manifestVersion!) === 1; const targetVersion = manifestVersion || inferredVersion; const commitsSinceLastTag = selectImageRelevantCommits(commits.sinceLastTag ?? []); @@ -174,9 +234,8 @@ export function buildOrbReleaseReport({ tags, manifestVersion, commits }) { // null (not just matching major.minor.patch) is what keeps a stable tag from being misread as "the beta // to continue counting from." const anyTagBeta = anyTag ? parseOrbBetaVersion(anyTag.version) : null; - const anyTagIsBetaOfTargetVersion = - anyTagBeta !== null && anyTagBeta.betaNumber !== null && `${anyTagBeta.major}.${anyTagBeta.minor}.${anyTagBeta.patch}` === targetVersion; - const nextBetaNumber = anyTagIsBetaOfTargetVersion ? anyTagBeta.betaNumber + 1 : 1; + const anyTagIsBetaOfTargetVersion = anyTagBeta !== null && anyTagBeta.betaNumber !== null && `${anyTagBeta.major}.${anyTagBeta.minor}.${anyTagBeta.patch}` === targetVersion; + const nextBetaNumber = anyTagIsBetaOfTargetVersion ? anyTagBeta!.betaNumber! + 1 : 1; return { due, @@ -201,7 +260,7 @@ export function buildOrbReleaseReport({ tags, manifestVersion, commits }) { * into orb-manifest.json is itself the human-reviewable gate, so there's nothing left here to compare it * against. */ -export function buildOrbStableReleaseReport({ tags, commitsSinceStable }) { +export function buildOrbStableReleaseReport({ tags, commitsSinceStable }: { tags: readonly string[]; commitsSinceStable?: OrbReleaseCommit[] }): OrbStableReleaseReport { const stableTag = latestStableOrbTag(tags); const stableVersion = stableTag?.version ?? "0.0.0"; diff --git a/scripts/smoke-production-versions.d.mts b/scripts/smoke-production-versions.d.mts deleted file mode 100644 index 1da5ab4165..0000000000 --- a/scripts/smoke-production-versions.d.mts +++ /dev/null @@ -1,9 +0,0 @@ -export const MINIMUM_SUPPORTED_MCP_VERSION: string; - -export type ExpectedMcpVersions = { - minimumSupportedVersion: string; - latestRecommendedVersion: string; -}; - -export function expectedMcpVersions(mcpPackageJson: { version?: unknown } | null | undefined): ExpectedMcpVersions; -export function loadMcpPackageJson(fromUrl?: string): { version: string; [key: string]: unknown }; diff --git a/scripts/smoke-production-versions.mjs b/scripts/smoke-production-versions.ts similarity index 75% rename from scripts/smoke-production-versions.mjs rename to scripts/smoke-production-versions.ts index db1d594b1f..8b2f834800 100644 --- a/scripts/smoke-production-versions.mjs +++ b/scripts/smoke-production-versions.ts @@ -9,11 +9,12 @@ import { fileURLToPath } from "node:url"; /** Matches `MINIMUM_SUPPORTED_MCP_VERSION` in src/services/mcp-compatibility.ts. */ export const MINIMUM_SUPPORTED_MCP_VERSION = "0.5.0"; -/** - * @param {{ version?: unknown } | null | undefined} mcpPackageJson - * @returns {{ minimumSupportedVersion: string, latestRecommendedVersion: string }} - */ -export function expectedMcpVersions(mcpPackageJson) { +export type ExpectedMcpVersions = { + minimumSupportedVersion: string; + latestRecommendedVersion: string; +}; + +export function expectedMcpVersions(mcpPackageJson: { version?: unknown } | null | undefined): ExpectedMcpVersions { const version = typeof mcpPackageJson?.version === "string" ? mcpPackageJson.version.trim() : ""; if (!version) throw new Error("packages/loopover-mcp/package.json is missing a non-empty version"); return { @@ -23,7 +24,7 @@ export function expectedMcpVersions(mcpPackageJson) { } /** Load the MCP package.json next to this repo's scripts/ directory. */ -export function loadMcpPackageJson(fromUrl = import.meta.url) { +export function loadMcpPackageJson(fromUrl: string = import.meta.url): { version: string; [key: string]: unknown } { const packagePath = join(dirname(fileURLToPath(fromUrl)), "../packages/loopover-mcp/package.json"); return JSON.parse(readFileSync(packagePath, "utf8")); } diff --git a/scripts/smoke-production.mjs b/scripts/smoke-production.ts similarity index 83% rename from scripts/smoke-production.mjs rename to scripts/smoke-production.ts index ffb8778bac..56f1034ce1 100644 --- a/scripts/smoke-production.mjs +++ b/scripts/smoke-production.ts @@ -1,4 +1,4 @@ -import { expectedMcpVersions, loadMcpPackageJson } from "./smoke-production-versions.mjs"; +import { expectedMcpVersions, loadMcpPackageJson } from "./smoke-production-versions.js"; const siteOrigin = normalizeOrigin(process.env.LOOPOVER_SITE_ORIGIN ?? "https://loopover.ai"); const apiOrigin = normalizeOrigin(process.env.LOOPOVER_API_ORIGIN ?? "https://api.loopover.ai"); @@ -29,7 +29,7 @@ const siteRoutes = [ await main(); async function main() { - const checks = []; + const checks: Promise[] = []; for (const route of siteRoutes) checks.push(checkStatus(`${siteOrigin}${route}`, 200, `UI ${route}`)); checks.push(checkStatus(`${siteOrigin}/openapi.json`, 200, "UI OpenAPI artifact", { contentType: /application\/json/i })); checks.push(checkStatus(`${siteOrigin}/downloads/loopover-extension.zip`, 200, "extension zip", { contentType: /application\/zip/i })); @@ -58,15 +58,17 @@ async function main() { ), ); checks.push(checkJson(`${apiOrigin}/v1/auth/session`, "signed-out session", (json) => json.status === "signed_out")); - checks.push(checkJson(`${apiOrigin}/v1/repos`, "protected API auth", (json, response) => response.status === 401 && json.error === "unauthorized", { expectedStatus: 401 })); + checks.push( + checkJson(`${apiOrigin}/v1/repos`, "protected API auth", (json, response) => response.status === 401 && json.error === "unauthorized", { + expectedStatus: 401, + }), + ); checks.push(checkRedirect(`${apiOrigin}/v1/auth/github/start`, "GitHub OAuth start", /^https:\/\/github\.com\/login\/oauth\/authorize\?/)); checks.push(checkCors(true)); checks.push(checkCors(false)); const results = await Promise.allSettled(checks); - const failures = results - .map((result) => (result.status === "rejected" ? result.reason : null)) - .filter(Boolean); + const failures = results.map((result) => (result.status === "rejected" ? (result.reason as unknown) : null)).filter(Boolean); if (failures.length > 0) { for (const failure of failures) console.error(`production smoke failed: ${failure instanceof Error ? failure.message : String(failure)}`); process.exitCode = 1; @@ -75,20 +77,25 @@ async function main() { console.log(`production smoke passed for ${siteOrigin} and ${apiOrigin}`); } -async function checkStatus(url, expectedStatus, label, options = {}) { +async function checkStatus(url: string, expectedStatus: number, label: string, options: { contentType?: RegExp } = {}) { const response = await fetchWithTimeout(url); assert(response.status === expectedStatus, `${label} expected ${expectedStatus}, got ${response.status}`); if (options.contentType) assert(options.contentType.test(response.headers.get("content-type") ?? ""), `${label} had unexpected content-type ${response.headers.get("content-type")}`); } -async function checkText(url, expectedText, label) { +async function checkText(url: string, expectedText: string, label: string) { const response = await fetchWithTimeout(url); assert(response.ok, `${label} expected success, got ${response.status}`); const text = await response.text(); assert(text.includes(expectedText), `${label} missing ${expectedText}`); } -async function checkJson(url, label, predicate, options = {}) { +async function checkJson( + url: string, + label: string, + predicate: (json: any, response: Response) => boolean, + options: { expectedStatus?: number } = {}, +) { const response = await fetchWithTimeout(url); const expectedStatus = options.expectedStatus ?? 200; assert(response.status === expectedStatus, `${label} expected ${expectedStatus}, got ${response.status}`); @@ -96,14 +103,14 @@ async function checkJson(url, label, predicate, options = {}) { assert(predicate(json, response), `${label} returned unexpected JSON ${JSON.stringify(json).slice(0, 500)}`); } -async function checkRedirect(url, label, locationPattern) { +async function checkRedirect(url: string, label: string, locationPattern: RegExp) { const response = await fetchWithTimeout(url, { redirect: "manual" }); assert(response.status === 302, `${label} expected 302, got ${response.status}`); const location = response.headers.get("location") ?? ""; assert(locationPattern.test(location), `${label} redirected to unexpected location ${location}`); } -async function checkCors(allowed) { +async function checkCors(allowed: boolean) { const origin = allowed ? siteOrigin : "https://evil.example"; const response = await fetchWithTimeout(`${apiOrigin}/v1/auth/session`, { headers: { origin } }); const allowOrigin = response.headers.get("access-control-allow-origin"); @@ -117,7 +124,7 @@ async function checkCors(allowed) { assert(!allowCredentials, `untrusted CORS credentials were allowed: ${allowCredentials}`); } -async function fetchWithTimeout(url, init = {}) { +async function fetchWithTimeout(url: string, init: RequestInit = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 20_000); try { @@ -134,11 +141,11 @@ async function fetchWithTimeout(url, init = {}) { } } -function normalizeOrigin(value) { +function normalizeOrigin(value: string) { const url = new URL(value); return url.origin; } -function assert(condition, message) { +function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); } diff --git a/scripts/sync-release-lockfile-versions.d.mts b/scripts/sync-release-lockfile-versions.d.mts deleted file mode 100644 index 880e607797..0000000000 --- a/scripts/sync-release-lockfile-versions.d.mts +++ /dev/null @@ -1,32 +0,0 @@ -export type SyncLockfileWorkspace = { - workspacePath: string; - version: string; -}; - -export type SyncLockfileHooks = { - onFailure?: (workspacePath: string) => void; - onAlready?: (workspacePath: string, version: string) => void; - onSynced?: (workspacePath: string, version: string) => void; -}; - -export type SyncLockfileResult = { - content: string; - changed: boolean; - failures: string[]; -}; - -export function syncLockfileVersions( - content: string, - workspaces: SyncLockfileWorkspace[], - hooks?: SyncLockfileHooks, -): SyncLockfileResult; - -export type SyncLockfileIo = { - readFileSync: (path: string, encoding: string) => string; - writeFileSync: (path: string, content: string) => void; - log: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - exit: (code: number) => void; -}; - -export function main(argv?: string[], io?: SyncLockfileIo): number; diff --git a/scripts/sync-release-lockfile-versions.mjs b/scripts/sync-release-lockfile-versions.ts similarity index 70% rename from scripts/sync-release-lockfile-versions.mjs rename to scripts/sync-release-lockfile-versions.ts index 89fefcfde4..a2693db300 100644 --- a/scripts/sync-release-lockfile-versions.mjs +++ b/scripts/sync-release-lockfile-versions.ts @@ -10,24 +10,31 @@ import { readFileSync, writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; +export type SyncLockfileWorkspace = { + workspacePath: string; + version: string; +}; + +export type SyncLockfileHooks = { + onFailure?: (workspacePath: string) => void; + onAlready?: (workspacePath: string, version: string) => void; + onSynced?: (workspacePath: string, version: string) => void; +}; + +export type SyncLockfileResult = { + content: string; + changed: boolean; + failures: string[]; +}; + /** * Sync each workspace's lockfile block version. Continues past match failures so one broken * workspace doesn't hide others; callers must exit non-zero when `failures` is non-empty (#6296). - * - * @param {string} content - * @param {Array<{ workspacePath: string, version: string }>} workspaces - * @param {{ - * onFailure?: (workspacePath: string) => void, - * onAlready?: (workspacePath: string, version: string) => void, - * onSynced?: (workspacePath: string, version: string) => void, - * }} [hooks] - * @returns {{ content: string, changed: boolean, failures: string[] }} */ -export function syncLockfileVersions(content, workspaces, hooks = {}) { +export function syncLockfileVersions(content: string, workspaces: SyncLockfileWorkspace[], hooks: SyncLockfileHooks = {}): SyncLockfileResult { let next = content; let changed = false; - /** @type {string[]} */ - const failures = []; + const failures: string[] = []; for (const { workspacePath, version } of workspaces) { const escapedKey = workspacePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -52,16 +59,27 @@ export function syncLockfileVersions(content, workspaces, hooks = {}) { return { content: next, changed, failures }; } -export function main(argv = process.argv.slice(2), io = { - readFileSync, - writeFileSync, - log: console.log.bind(console), - error: console.error.bind(console), - exit: (code) => process.exit(code), -}) { +export type SyncLockfileIo = { + readFileSync: (path: string, encoding: string) => string; + writeFileSync: (path: string, content: string) => void; + log: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + exit: (code: number) => void; +}; + +export function main( + argv: string[] = process.argv.slice(2), + io: SyncLockfileIo = { + readFileSync: (path: string, encoding: string) => readFileSync(path, encoding as BufferEncoding), + writeFileSync, + log: console.log.bind(console), + error: console.error.bind(console), + exit: (code: number) => process.exit(code), + }, +): number { const targets = argv; if (targets.length === 0) { - io.error("Usage: node sync-release-lockfile-versions.mjs [ ...]"); + io.error("Usage: tsx sync-release-lockfile-versions.ts [ ...]"); io.exit(1); return 1; } diff --git a/scripts/sync-release-manifest.d.mts b/scripts/sync-release-manifest.d.mts deleted file mode 100644 index 32a5ae9c77..0000000000 --- a/scripts/sync-release-manifest.d.mts +++ /dev/null @@ -1,28 +0,0 @@ -export const MANIFEST_PATH: string; - -export type SyncManifestStaleEntry = { - workspacePath: string; - from: string; - to: string; -}; - -export type SyncManifestResult = { - content: string; - changed: boolean; - stale: SyncManifestStaleEntry[]; -}; - -export function syncManifestVersions( - manifestJson: string, - packageVersions: Record, -): SyncManifestResult; - -export type SyncManifestIo = { - readFileSync: (path: string, encoding: string) => string; - writeFileSync: (path: string, content: string) => void; - log: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - exit: (code: number) => void; -}; - -export function main(argv?: string[], io?: SyncManifestIo): number; diff --git a/scripts/sync-release-manifest.mjs b/scripts/sync-release-manifest.ts similarity index 68% rename from scripts/sync-release-manifest.mjs rename to scripts/sync-release-manifest.ts index d129495326..74fcca8302 100644 --- a/scripts/sync-release-manifest.mjs +++ b/scripts/sync-release-manifest.ts @@ -18,15 +18,22 @@ import { pathToFileURL } from "node:url"; export const MANIFEST_PATH = ".release-please-manifest.json"; -/** - * Pure: diff a manifest's recorded versions against each workspace's real package.json version. - * @param {string} manifestJson - * @param {Record} packageVersions workspacePath -> package.json's "version" - * @returns {{ content: string, changed: boolean, stale: Array<{ workspacePath: string, from: string, to: string }> }} - */ -export function syncManifestVersions(manifestJson, packageVersions) { +export type SyncManifestStaleEntry = { + workspacePath: string; + from: string; + to: string; +}; + +export type SyncManifestResult = { + content: string; + changed: boolean; + stale: SyncManifestStaleEntry[]; +}; + +/** Pure: diff a manifest's recorded versions against each workspace's real package.json version. */ +export function syncManifestVersions(manifestJson: string, packageVersions: Record): SyncManifestResult { const manifest = JSON.parse(manifestJson); - const stale = []; + const stale: SyncManifestStaleEntry[] = []; for (const [workspacePath, version] of Object.entries(packageVersions)) { if (!(workspacePath in manifest)) continue; // not a manifest-tracked component -- nothing to sync if (manifest[workspacePath] === version) continue; @@ -37,21 +44,29 @@ export function syncManifestVersions(manifestJson, packageVersions) { return { content: changed ? `${JSON.stringify(manifest, null, 2)}\n` : manifestJson, changed, stale }; } -export function main(argv = process.argv.slice(2), io = { - readFileSync, - writeFileSync, - log: console.log.bind(console), - error: console.error.bind(console), - exit: (code) => process.exit(code), -}) { +export type SyncManifestIo = { + readFileSync: (path: string, encoding: string) => string; + writeFileSync: (path: string, content: string) => void; + log: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + exit: (code: number) => void; +}; + +export function main( + argv: string[] = process.argv.slice(2), + io: SyncManifestIo = { + readFileSync: (path: string, encoding: string) => readFileSync(path, encoding as BufferEncoding), + writeFileSync, + log: console.log.bind(console), + error: console.error.bind(console), + exit: (code: number) => process.exit(code), + }, +): number { const check = argv.includes("--check"); const manifestJson = io.readFileSync(MANIFEST_PATH, "utf8"); const manifestKeys = Object.keys(JSON.parse(manifestJson)); const packageVersions = Object.fromEntries( - manifestKeys.map((workspacePath) => [ - workspacePath, - JSON.parse(io.readFileSync(`${workspacePath}/package.json`, "utf8")).version, - ]), + manifestKeys.map((workspacePath) => [workspacePath, JSON.parse(io.readFileSync(`${workspacePath}/package.json`, "utf8")).version]), ); const result = syncManifestVersions(manifestJson, packageVersions); @@ -68,9 +83,7 @@ export function main(argv = process.argv.slice(2), io = { return 0; } if (result.changed) io.writeFileSync(MANIFEST_PATH, result.content); - io.log( - `sync-release-manifest: ${result.changed ? `synced ${result.stale.length}` : "checked"} of ${manifestKeys.length} package version(s).`, - ); + io.log(`sync-release-manifest: ${result.changed ? `synced ${result.stale.length}` : "checked"} of ${manifestKeys.length} package version(s).`); return 0; } diff --git a/scripts/validate-observability-configs.d.mts b/scripts/validate-observability-configs.d.mts deleted file mode 100644 index 89b17aad3a..0000000000 --- a/scripts/validate-observability-configs.d.mts +++ /dev/null @@ -1,3 +0,0 @@ -export declare function validateDashboards(dir: string): string[]; - -export declare function validateAlertRules(path: string): string[]; diff --git a/scripts/validate-observability-configs.mjs b/scripts/validate-observability-configs.ts similarity index 76% rename from scripts/validate-observability-configs.mjs rename to scripts/validate-observability-configs.ts index a3d57c13b7..3449ce3265 100644 --- a/scripts/validate-observability-configs.mjs +++ b/scripts/validate-observability-configs.ts @@ -16,23 +16,29 @@ import { parse as parseYaml } from "yaml"; // Valid JSON/YAML can parse to a non-object (null, a string, a number, an array of non-objects) -- // dereferencing a property on that crashes instead of producing a validation error. Every dereference // below goes through this guard first. -function isObject(value) { +function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +// Mirrors `container?.[key]` under `unknown` typing (isObject's null/array exclusion matches what +// optional chaining already treats as "nothing to read from" for a non-object container). +function readProp(container: unknown, key: string): unknown { + return isObject(container) ? container[key] : undefined; +} + const BINARY_OPERATORS = ["==", "!=", ">=", "<=", ">", "<", "+", "-", "*", "/", "%", "^", "and", "or", "unless"]; // A deliberately lightweight PromQL sanity check -- NOT a real parser (no promtool dependency; see the // module doc comment). Catches the class of mistake a copy-paste/typo produces: unbalanced brackets, or // an expression left dangling on a binary operator with no right-hand side (e.g. "up =="). -function promqlSanityIssue(expr) { +function promqlSanityIssue(expr: string): string | null { const trimmed = expr.trim(); // A stack, not a depth counter: a depth counter only checks NESTING COUNT, so "sum(foo[5m))" -- opened // with "(" and "[", closed with ")" and ")" -- reaches depth 0 and would wrongly look balanced. The // stack checks each closer against the delimiter it's actually supposed to match. - const pairs = { ")": "(", "]": "[", "}": "{" }; + const pairs: Record = { ")": "(", "]": "[", "}": "{" }; const opens = new Set(["(", "[", "{"]); - const stack = []; + const stack: string[] = []; for (const ch of trimmed) { if (opens.has(ch)) { stack.push(ch); @@ -51,22 +57,22 @@ function promqlSanityIssue(expr) { return null; } -export function validateDashboards(dir) { - const errors = []; - let files; +export function validateDashboards(dir: string): string[] { + const errors: string[] = []; + let files: string[]; try { files = readdirSync(dir).filter((f) => f.endsWith(".json")); } catch (error) { - return [`${dir}: could not read directory — ${error.message}`]; + return [`${dir}: could not read directory — ${(error as Error).message}`]; } if (files.length === 0) errors.push(`${dir}: no dashboard JSON files found`); for (const file of files) { const path = `${dir}/${file}`; - let dashboard; + let dashboard: unknown; try { dashboard = JSON.parse(readFileSync(path, "utf8")); } catch (error) { - errors.push(`${path}: invalid JSON — ${error.message}`); + errors.push(`${path}: invalid JSON — ${(error as Error).message}`); continue; } if (!isObject(dashboard)) { @@ -83,18 +89,19 @@ export function validateDashboards(dir) { return errors; } -export function validateAlertRules(path) { - const errors = []; - let doc; +export function validateAlertRules(path: string): string[] { + const errors: string[] = []; + let doc: unknown; try { doc = parseYaml(readFileSync(path, "utf8")); } catch (error) { - return [`${path}: invalid YAML — ${error.message}`]; + return [`${path}: invalid YAML — ${(error as Error).message}`]; } - if (!Array.isArray(doc?.groups)) { + const groups = readProp(doc, "groups") as unknown[]; + if (!Array.isArray(groups)) { return [`${path}: missing a top-level "groups" array`]; } - for (const [groupIndex, group] of doc.groups.entries()) { + for (const [groupIndex, group] of groups.entries()) { if (!isObject(group)) { errors.push(`${path}: groups[${groupIndex}] must be an object, not ${JSON.stringify(group)}`); continue; @@ -102,15 +109,14 @@ export function validateAlertRules(path) { if (typeof group.name !== "string" || !group.name) { errors.push(`${path}: a group is missing a non-empty "name"`); } - if (!Array.isArray(group.rules)) { + const rules = group.rules as unknown[]; + if (!Array.isArray(rules)) { errors.push(`${path}: group "${group.name ?? "?"}" is missing a "rules" array`); continue; } - for (const [ruleIndex, rule] of group.rules.entries()) { + for (const [ruleIndex, rule] of rules.entries()) { if (!isObject(rule)) { - errors.push( - `${path}: group "${group.name}" rules[${ruleIndex}] must be an object, not ${JSON.stringify(rule)}`, - ); + errors.push(`${path}: group "${group.name}" rules[${ruleIndex}] must be an object, not ${JSON.stringify(rule)}`); continue; } const label = rule.alert ?? "(unnamed rule)"; @@ -123,10 +129,12 @@ export function validateAlertRules(path) { const issue = promqlSanityIssue(rule.expr); if (issue) errors.push(`${path}: rule "${label}" has a suspect "expr" — ${issue}`); } - if (typeof rule.labels?.severity !== "string" || !rule.labels.severity) { + const severity = readProp(rule.labels, "severity"); + if (typeof severity !== "string" || !severity) { errors.push(`${path}: rule "${label}" is missing "labels.severity"`); } - if (typeof rule.annotations?.summary !== "string" || !rule.annotations.summary) { + const summary = readProp(rule.annotations, "summary"); + if (typeof summary !== "string" || !summary) { errors.push(`${path}: rule "${label}" is missing "annotations.summary"`); } } diff --git a/scripts/validate-selfhost-sourcemap.d.mts b/scripts/validate-selfhost-sourcemap.d.mts deleted file mode 100644 index dd14c74b08..0000000000 --- a/scripts/validate-selfhost-sourcemap.d.mts +++ /dev/null @@ -1,8 +0,0 @@ -export type ValidateSourcemapOptions = { - bundlePath?: string; - mapPath?: string; - exists?: (path: string) => boolean; - readFile?: (path: string) => string; -}; - -export declare function validateSourcemap(options?: ValidateSourcemapOptions): { sourceCount: number }; diff --git a/scripts/validate-selfhost-sourcemap.mjs b/scripts/validate-selfhost-sourcemap.ts similarity index 69% rename from scripts/validate-selfhost-sourcemap.mjs rename to scripts/validate-selfhost-sourcemap.ts index 4e2266dccc..eb71584e0c 100644 --- a/scripts/validate-selfhost-sourcemap.mjs +++ b/scripts/validate-selfhost-sourcemap.ts @@ -10,20 +10,24 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; -/** - * @param {{ - * bundlePath?: string, - * mapPath?: string, - * exists?: (path: string) => boolean, - * readFile?: (path: string) => string, - * }} [options] - * @returns {{ sourceCount: number }} - */ -export function validateSourcemap(options = {}) { +export type ValidateSourcemapOptions = { + bundlePath?: string; + mapPath?: string; + exists?: (path: string) => boolean; + readFile?: (path: string) => string; +}; + +type SourceMapLike = { + version?: unknown; + sources?: unknown; + sourcesContent?: unknown; +}; + +export function validateSourcemap(options: ValidateSourcemapOptions = {}): { sourceCount: number } { const bundlePath = options.bundlePath ?? resolve(process.cwd(), "dist/server.mjs"); const mapPath = options.mapPath ?? resolve(process.cwd(), "dist/server.mjs.map"); const exists = options.exists ?? existsSync; - const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8")); + const readFile = options.readFile ?? ((path: string) => readFileSync(path, "utf8")); if (!exists(bundlePath)) throw new Error("dist/server.mjs is missing"); if (!exists(mapPath)) throw new Error("dist/server.mjs.map is missing"); @@ -33,13 +37,11 @@ export function validateSourcemap(options = {}) { throw new Error("dist/server.mjs is missing the server.mjs.map sourceMappingURL"); } - let map; + let map: SourceMapLike; try { map = JSON.parse(readFile(mapPath)); } catch (error) { - throw new Error( - `dist/server.mjs.map is not valid JSON (${error instanceof Error ? error.message : String(error)})`, - ); + throw new Error(`dist/server.mjs.map is not valid JSON (${error instanceof Error ? error.message : String(error)})`); } if (map.version !== 3) throw new Error("dist/server.mjs.map is not a version 3 source map"); @@ -49,32 +51,30 @@ export function validateSourcemap(options = {}) { if (!Array.isArray(map.sourcesContent) || map.sourcesContent.length !== map.sources.length) { throw new Error("dist/server.mjs.map must include sourcesContent for every original source"); } - const serverSourceIndex = map.sources.findIndex((source) => String(source).endsWith("src/server.ts")); + const sources: unknown[] = map.sources; + const sourcesContent: unknown[] = map.sourcesContent; + const serverSourceIndex = sources.findIndex((source) => String(source).endsWith("src/server.ts")); if (serverSourceIndex === -1) { throw new Error("dist/server.mjs.map does not include src/server.ts"); } - if (map.sourcesContent[serverSourceIndex]?.trim() === "") { + if (String(sourcesContent[serverSourceIndex] ?? "").trim() === "") { throw new Error("dist/server.mjs.map has empty source content for src/server.ts"); } - const repoSourceIndexes = map.sources - .map((source, index) => [String(source), index]) + const repoSourceIndexes = sources + .map((source, index): [string, number] => [String(source), index]) .filter(([source]) => source.startsWith("../src/")) .map(([, index]) => index); if (repoSourceIndexes.length === 0) { throw new Error("dist/server.mjs.map does not include repository sources"); } - if ( - repoSourceIndexes.some( - (index) => typeof map.sourcesContent[index] !== "string" || map.sourcesContent[index].trim() === "", - ) - ) { + if (repoSourceIndexes.some((index) => typeof sourcesContent[index] !== "string" || (sourcesContent[index] as string).trim() === "")) { throw new Error("dist/server.mjs.map is missing source content for a repository source"); } - return { sourceCount: map.sources.length }; + return { sourceCount: sources.length }; } -function fail(message) { +function fail(message: string): never { console.error(`self-host sourcemap validation failed: ${message}`); process.exit(1); } diff --git a/src/selfhost/docs-drift-audit-sweep-rfc.md b/src/selfhost/docs-drift-audit-sweep-rfc.md index dcb4e99eb8..89976f6678 100644 --- a/src/selfhost/docs-drift-audit-sweep-rfc.md +++ b/src/selfhost/docs-drift-audit-sweep-rfc.md @@ -1,7 +1,7 @@ # Scheduled docs-drift audit sweep — design doc (#3048) Part of #1819 (self-host docs accuracy audit). Siblings: the mechanical presence/shape drift lint -(`scripts/check-docs-drift.mjs`, "Phase 1") and schema-anchored examples ("Phase 2"). This RFC covers +(`scripts/check-docs-drift.ts`, "Phase 1") and schema-anchored examples ("Phase 2"). This RFC covers the catch-all Phase 3: a recurring, low-noise sweep that catches **behavioral/prose** drift — a docs claim that was true when written and quietly stopped being true — which neither sibling can catch mechanically. Per the issue's own deliverables, this doc is the first step; the actual scheduled job @@ -9,11 +9,11 @@ lands once the design below is agreed. ## What the two mechanical siblings already catch (and don't) -`scripts/check-docs-drift.mjs` cross-checks five enumerable **presence** surfaces against specific docs +`scripts/check-docs-drift.ts` cross-checks five enumerable **presence** surfaces against specific docs pages/examples: `LOOPOVER_REVIEW_*` feature flags (`src/env.d.ts`), `@loopover` command catalogs (`src/github/commands.ts`), `*GateMode` fields (`src/types.ts`'s `RepositorySettings`), and the full `RepositorySettings` + `FocusManifest` field surface against `.loopover.yml.example` -(`check-docs-drift.mjs:1-11`). It answers "does every flag/command/field that exists in code also appear +(`check-docs-drift.ts:1-11`). It answers "does every flag/command/field that exists in code also appear somewhere in docs?" — a shape/presence check, anchored on declaration syntax (`extractLoopOverReviewFlags`, `extractGateModeFields`, etc.), not on whether the *prose describing* that surface is still accurate. diff --git a/test/helpers/vitest-global-setup-node-version.ts b/test/helpers/vitest-global-setup-node-version.ts index d3c03f53bf..e0e3bde501 100644 --- a/test/helpers/vitest-global-setup-node-version.ts +++ b/test/helpers/vitest-global-setup-node-version.ts @@ -1,8 +1,8 @@ -import { checkNodeVersion } from "../../scripts/check-node-version.mjs"; +import { checkNodeVersion } from "../../scripts/check-node-version.js"; /** * Vitest `globalSetup` -- the only mechanism that covers *every* vitest invocation path, not just - * specific npm script names. scripts/check-node-version.mjs's `pretest*` hooks (package.json) only fire + * specific npm script names. scripts/check-node-version.ts's `pretest*` hooks (package.json) only fire * for the exact npm script names they're wired to -- they can never cover a direct * `npx vitest run test/unit/.test.ts` invocation, which the contributing skill docs explicitly * recommend for fast iteration (reference.md, SKILL.md). A globalSetup runs once before any test file, diff --git a/test/unit/alerts-metric-name-references.test.ts b/test/unit/alerts-metric-name-references.test.ts index 04034f0a7e..3057a99e12 100644 --- a/test/unit/alerts-metric-name-references.test.ts +++ b/test/unit/alerts-metric-name-references.test.ts @@ -5,7 +5,7 @@ import { DEFAULT_METRIC_META } from "../../src/selfhost/metrics"; // Regression check (#5816): a hand-authored alert annotation (summary/description/runbook) can reference a // `loopover_*` metric name that was never registered, or was renamed/removed, without any existing tooling -// catching it -- scripts/validate-observability-configs.mjs only validates PromQL `expr` syntax, never +// catching it -- scripts/validate-observability-configs.ts only validates PromQL `expr` syntax, never // free-text annotation prose. Scans every `loopover_*`-shaped token in every alert's annotation text and // asserts it is either a real registered metric (src/selfhost/metrics.ts's DEFAULT_METRIC_META) or matches // one of the documented external-source prefixes (the backup exporter, the opt-in Cloudflare D1 probe, and diff --git a/test/unit/build-extension-zip-script.test.ts b/test/unit/build-extension-zip-script.test.ts index b634b39493..9ca3a93706 100644 --- a/test/unit/build-extension-zip-script.test.ts +++ b/test/unit/build-extension-zip-script.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { crc32, createStoredZip, listFiles } from "../../scripts/extension-zip-core.mjs"; +import { crc32, createStoredZip, listFiles } from "../../scripts/extension-zip-core.js"; // Minimal reader for the STORED (uncompressed) archives createStoredZip writes. It walks the // end-of-central-directory + central directory rather than trusting insertion order, so a wrong @@ -53,7 +53,7 @@ function readStoredZip(zip: Buffer): ExtractedEntry[] { return entries; } -describe("build-extension.mjs zip writer (#7464)", () => { +describe("build-extension.ts zip writer (#7464)", () => { describe("crc32", () => { it("matches the canonical CRC-32 test vector", () => { // crc32("123456789") === 0xCBF43926 is the standard reference vector for CRC-32/ISO-HDLC. diff --git a/test/unit/check-branding-drift-script.test.ts b/test/unit/check-branding-drift-script.test.ts index fdace9fe1f..db556a7d06 100644 --- a/test/unit/check-branding-drift-script.test.ts +++ b/test/unit/check-branding-drift-script.test.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { describe, expect, it } from "vitest"; -import { diffBrandingBaseline, scanBrandingHits } from "../../scripts/check-branding-drift.mjs"; +import { diffBrandingBaseline, scanBrandingHits } from "../../scripts/check-branding-drift.js"; describe("scanBrandingHits", () => { it("parses git grep -c output into a { file: count } map", () => { @@ -122,7 +122,7 @@ describe("check-branding-drift script (real repo state)", () => { // now. If this fails, real drift landed (or a cleanup did) without regenerating the baseline -- either way, // fix it with `npm run branding-drift:update`, don't weaken this test. it("the committed baseline matches the real current repo state (regression guard)", () => { - const output = execFileSync("node", ["scripts/check-branding-drift.mjs"], { encoding: "utf8" }); + const output = execFileSync(process.execPath, ["--experimental-strip-types", "scripts/check-branding-drift.ts"], { encoding: "utf8" }); expect(output).toMatch(/Branding-drift check ok: \d+ file\(s\) match the recorded baseline\./); }); diff --git a/test/unit/check-build-drift-script.test.ts b/test/unit/check-build-drift-script.test.ts deleted file mode 100644 index e7ca0df9aa..0000000000 --- a/test/unit/check-build-drift-script.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { checkBuildDrift, pathsForPackage } from "../../scripts/check-build-drift.mjs"; - -const tmpDirs: string[] = []; -afterEach(() => { - for (const dir of tmpDirs.splice(0)) rmSync(dir, { recursive: true, force: true }); -}); - -function initScratchGitRepo(): string { - const dir = mkdtempSync(join(tmpdir(), "gtbuild-drift-")); - tmpDirs.push(dir); - const git = (...args: string[]) => execFileSync("git", args, { cwd: dir, encoding: "utf8" }); - git("init", "--quiet"); - git("-c", "user.email=test@example.com", "-c", "user.name=test", "-c", "commit.gpgsign=false", "commit", "--allow-empty", "-m", "init", "--quiet"); - return dir; -} - -describe("check-build-drift script", () => { - describe("pathsForPackage", () => { - it("resolves the real loopover-miner bin/lib paths", () => { - expect(pathsForPackage("miner")).toEqual(["packages/loopover-miner/bin", "packages/loopover-miner/lib"]); - }); - - it("resolves the real loopover-mcp bin/lib paths", () => { - expect(pathsForPackage("mcp")).toEqual(["packages/loopover-mcp/bin", "packages/loopover-mcp/lib"]); - }); - - it("throws a clear error for an unknown package name", () => { - expect(() => pathsForPackage("engine")).toThrow(/unknown package "engine".*miner.*mcp/s); - }); - }); - - describe("checkBuildDrift (injected run)", () => { - it("returns empty when the injected git status reports nothing", () => { - const drift = checkBuildDrift(["packages/loopover-miner/lib"], { run: () => "" }); - expect(drift).toBe(""); - }); - - it("trims and returns whatever the injected git status reports", () => { - const drift = checkBuildDrift(["packages/loopover-miner/lib"], { - run: () => "\n M packages/loopover-miner/lib/foo.js\n?? packages/loopover-miner/lib/foo.d.ts\n\n", - }); - expect(drift).toBe("M packages/loopover-miner/lib/foo.js\n?? packages/loopover-miner/lib/foo.d.ts"); - }); - - it("passes the paths and cwd it was given through to run", () => { - const calls: Array<{ paths: string[]; cwd: string }> = []; - checkBuildDrift(["bin", "lib"], { - cwd: "/fake/repo", - run: (paths, cwd) => { - calls.push({ paths, cwd }); - return ""; - }, - }); - expect(calls).toEqual([{ paths: ["bin", "lib"], cwd: "/fake/repo" }]); - }); - }); - - describe("checkBuildDrift (real git, scratch repo)", () => { - it("reports clean when nothing has changed since the last commit", () => { - const dir = initScratchGitRepo(); - expect(checkBuildDrift(["lib"], { cwd: dir })).toBe(""); - }); - - it("reports a modified tracked file (the 'stale committed .js' case)", () => { - const dir = initScratchGitRepo(); - const git = (...args: string[]) => execFileSync("git", args, { cwd: dir, encoding: "utf8" }); - writeFileSync(join(dir, "foo.js"), "export const x = 1;\n"); - git("add", "foo.js"); - git("-c", "user.email=test@example.com", "-c", "user.name=test", "-c", "commit.gpgsign=false", "commit", "-m", "add foo.js", "--quiet"); - - writeFileSync(join(dir, "foo.js"), "export const x = 2;\n"); - - expect(checkBuildDrift(["foo.js"], { cwd: dir })).toContain("foo.js"); - }); - - it("reports a brand new untracked file (the 'compiled output never committed' case)", () => { - // git diff --exit-code alone would miss this -- untracked files are invisible to `git diff`, - // which is exactly why this script uses `git status --porcelain` instead. - const dir = initScratchGitRepo(); - writeFileSync(join(dir, "bar.d.ts"), "export {};\n"); - - const drift = checkBuildDrift(["bar.d.ts"], { cwd: dir }); - - expect(drift).toContain("bar.d.ts"); - expect(drift.startsWith("??")).toBe(true); - }); - }); - - describe("CLI (real subprocess)", () => { - // Most important regression test in this file: proves the REAL currently-committed miner/mcp - // .js/.d.ts are not already stale relative to their .ts source -- if they were, this check would - // fail on `main` from the moment it merges. (Doesn't run the build itself -- npm run build:{miner,mcp} - // already ran as part of getting a green `npm run test:ci` locally before this change was committed.) - it("reports the real repo's loopover-miner output as clean", () => { - const output = execFileSync(process.execPath, ["scripts/check-build-drift.mjs", "miner"], { encoding: "utf8" }); - expect(output).toContain("packages/loopover-miner's committed .js/.d.ts matches its .ts source."); - }); - - it("reports the real repo's loopover-mcp output as clean", () => { - const output = execFileSync(process.execPath, ["scripts/check-build-drift.mjs", "mcp"], { encoding: "utf8" }); - expect(output).toContain("packages/loopover-mcp's committed .js/.d.ts matches its .ts source."); - }); - - it("exits non-zero with a clear message for an unknown package name", () => { - try { - execFileSync(process.execPath, ["scripts/check-build-drift.mjs", "engine"], { encoding: "utf8" }); - expect.unreachable("expected the CLI to exit non-zero for an unknown package name"); - } catch (err) { - const e = err as { status?: number; stdout?: string; stderr?: string }; - expect(e.status).toBe(1); - expect(`${e.stdout ?? ""}${e.stderr ?? ""}`).toContain('unknown package "engine"'); - } - }); - }); -}); diff --git a/test/unit/check-docs-drift-script.test.ts b/test/unit/check-docs-drift-script.test.ts index 76ac82e044..d0f10cbe71 100644 --- a/test/unit/check-docs-drift-script.test.ts +++ b/test/unit/check-docs-drift-script.test.ts @@ -10,7 +10,7 @@ import { FOCUS_MANIFEST_ALIAS_MANIFEST, GATE_MODE_MANIFEST, SETTINGS_ALIAS_MANIFEST, -} from "../../scripts/check-docs-drift.mjs"; +} from "../../scripts/check-docs-drift.js"; describe("check-docs-drift script", () => { describe("extractLoopOverReviewFlags", () => { @@ -555,7 +555,7 @@ describe("check-docs-drift script", () => { }); it("prints a clean summary and exits 0 for the real repo state when run as a subprocess", () => { - const output = execFileSync("node", ["scripts/check-docs-drift.mjs"], { encoding: "utf8" }); + const output = execFileSync("node", ["--experimental-strip-types", "scripts/check-docs-drift.ts"], { encoding: "utf8" }); expect(output).toMatch( /Docs-drift check ok: \d+ feature flags, \d+ commands, \d+ gate-mode fields, \d+ RepositorySettings fields, \d+ FocusManifest fields all documented\./, diff --git a/test/unit/check-engines-nvmrc-sync-script.test.ts b/test/unit/check-engines-nvmrc-sync-script.test.ts index 395ac89821..a2d3db9359 100644 --- a/test/unit/check-engines-nvmrc-sync-script.test.ts +++ b/test/unit/check-engines-nvmrc-sync-script.test.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { describe, expect, it } from "vitest"; -import { checkEnginesNvmrcSync } from "../../scripts/check-engines-nvmrc-sync.mjs"; +import { checkEnginesNvmrcSync } from "../../scripts/check-engines-nvmrc-sync.js"; describe("check-engines-nvmrc-sync script", () => { /** Builds `{ readFile, listDir }` fakes from a flat `{ "group/name/package.json": jsonString }` map @@ -140,7 +140,7 @@ describe("check-engines-nvmrc-sync script", () => { }); it("prints a clean summary and exits 0 for the real repo state when run as a subprocess", () => { - const output = execFileSync(process.execPath, ["scripts/check-engines-nvmrc-sync.mjs"], { encoding: "utf8" }); + const output = execFileSync(process.execPath, ["--experimental-strip-types", "scripts/check-engines-nvmrc-sync.ts"], { encoding: "utf8" }); expect(output).toMatch(/Engines\/\.nvmrc sync check ok: \d+ package\(s\)/); }); diff --git a/test/unit/check-manifest-drift-script.test.ts b/test/unit/check-manifest-drift-script.test.ts index 509bd57cc6..714b5fc81c 100644 --- a/test/unit/check-manifest-drift-script.test.ts +++ b/test/unit/check-manifest-drift-script.test.ts @@ -1,11 +1,11 @@ import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { checkManifestDrift } from "../../scripts/check-manifest-drift.mjs"; +import { checkManifestDrift } from "../../scripts/check-manifest-drift.js"; import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML } from "../../src/config/loopover-repo-focus-manifest"; // The script imports src/config/loopover-repo-focus-manifest.ts (a .ts module), so -- like -// check-schema-drift.mjs, check-migrations.mjs, and check-openapi-settings-parity.mjs -- it must run via +// check-schema-drift.ts, check-migrations.mjs, and check-openapi-settings-parity.ts -- it must run via // `tsx`, the same binary package.json's manifest:drift-check uses, rather than plain `node`. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); @@ -88,7 +88,7 @@ describe("check-manifest-drift script", () => { }); it("prints a clean summary and exits 0 for the real repo state when run as a subprocess", () => { - const output = execFileSync(TSX_BIN, ["scripts/check-manifest-drift.mjs"], { encoding: "utf8" }); + const output = execFileSync(TSX_BIN, ["scripts/check-manifest-drift.ts"], { encoding: "utf8" }); expect(output).toMatch(/Manifest-drift check ok: \.loopover\.yml and LOOPOVER_REPO_FOCUS_MANIFEST_YAML agree\./); }); diff --git a/test/unit/check-mcp-package.test.ts b/test/unit/check-mcp-package.test.ts index 09335e9f8e..673045b399 100644 --- a/test/unit/check-mcp-package.test.ts +++ b/test/unit/check-mcp-package.test.ts @@ -1,9 +1,14 @@ import { execFileSync } from "node:child_process"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; +// tsx, not plain node: check-mcp-package.mjs imports forbidden-content.ts and mcp-package-allowlist.ts +// directly, so plain node can't resolve those local .ts imports. +const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); + function runChecker(env: Record = {}): { status: number; out: string } { try { - const stdout = execFileSync(process.execPath, ["scripts/check-mcp-package.mjs"], { + const stdout = execFileSync(TSX_BIN, ["scripts/check-mcp-package.mjs"], { encoding: "utf8", env: { ...process.env, ...env }, }); diff --git a/test/unit/check-miner-deployment-docs.test.ts b/test/unit/check-miner-deployment-docs.test.ts index 251501ed5b..324ab93142 100644 --- a/test/unit/check-miner-deployment-docs.test.ts +++ b/test/unit/check-miner-deployment-docs.test.ts @@ -3,7 +3,7 @@ import { buildLiveMinerDeploymentReality, main, runMinerDeploymentDocsAudit, -} from "../../scripts/check-miner-deployment-docs.mjs"; +} from "../../scripts/check-miner-deployment-docs.js"; describe("check-miner-deployment-docs (#6158)", () => { it("passes against the live miner DEPLOYMENT.md and source tree", () => { diff --git a/test/unit/check-miner-package.test.ts b/test/unit/check-miner-package.test.ts index 14e634b8e0..ff3e653ecf 100644 --- a/test/unit/check-miner-package.test.ts +++ b/test/unit/check-miner-package.test.ts @@ -1,9 +1,14 @@ import { execFileSync } from "node:child_process"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; +// tsx, not plain node: check-miner-package.mjs imports forbidden-content.ts directly, so plain node can't +// resolve that local .ts import. +const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); + function runChecker(env: Record = {}): { status: number; out: string } { try { - const stdout = execFileSync(process.execPath, ["scripts/check-miner-package.mjs"], { + const stdout = execFileSync(TSX_BIN, ["scripts/check-miner-package.mjs"], { encoding: "utf8", env: { ...process.env, ...env }, }); diff --git a/test/unit/check-node-version-script.test.ts b/test/unit/check-node-version-script.test.ts index a691d3a6b6..15c0bb1639 100644 --- a/test/unit/check-node-version-script.test.ts +++ b/test/unit/check-node-version-script.test.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { describe, expect, it } from "vitest"; -import { checkNodeVersion } from "../../scripts/check-node-version.mjs"; +import { checkNodeVersion } from "../../scripts/check-node-version.js"; describe("check-node-version script", () => { it("passes when the running Node satisfies the declared engines.node range", () => { @@ -40,7 +40,7 @@ describe("check-node-version script", () => { }); it("prints nothing and exits 0 as a subprocess on a satisfying Node", () => { - const output = execFileSync(process.execPath, ["scripts/check-node-version.mjs"], { encoding: "utf8" }); + const output = execFileSync(process.execPath, ["--experimental-strip-types", "scripts/check-node-version.ts"], { encoding: "utf8" }); expect(output).toBe(""); }); diff --git a/test/unit/check-schema-drift-script.test.ts b/test/unit/check-schema-drift-script.test.ts index e60b3e2f65..386d3cbfb6 100644 --- a/test/unit/check-schema-drift-script.test.ts +++ b/test/unit/check-schema-drift-script.test.ts @@ -12,11 +12,11 @@ import { listActualTables, RAW_SQL_ONLY_TABLES, replayMigrations, -} from "../../scripts/check-schema-drift.mjs"; +} from "../../scripts/check-schema-drift.js"; import * as realSchema from "../../src/db/schema"; // #2565: the script imports src/db/schema.ts (a .ts module), so -- like check-migrations.mjs and -// check-openapi-settings-parity.mjs -- it must run via `tsx`, the same binary package.json's +// check-openapi-settings-parity.ts -- it must run via `tsx`, the same binary package.json's // db:schema-drift:check uses, rather than plain `node`. const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); @@ -37,7 +37,7 @@ describe("check-schema-drift script (#2565)", () => { }); it("prints a clean summary for the real repo state when run as a subprocess", () => { - const output = execFileSync(TSX_BIN, ["scripts/check-schema-drift.mjs"], { encoding: "utf8" }); + const output = execFileSync(TSX_BIN, ["scripts/check-schema-drift.ts"], { encoding: "utf8" }); expect(output).toMatch(/src\/db\/schema\.ts matches migrations\/ -- \d+ Drizzle tables OK/); }); @@ -88,7 +88,7 @@ describe("check-schema-drift script (#2565)", () => { const db = replayMigrations(dir); const mismatches = diffSchemaAgainstMigrations(db, {}); - expect(mismatches).toContain('table "undeclared_thing" exists in migrations/ but has no src/db/schema.ts declaration and is not in RAW_SQL_ONLY_TABLES (scripts/check-schema-drift.mjs)'); + expect(mismatches).toContain('table "undeclared_thing" exists in migrations/ but has no src/db/schema.ts declaration and is not in RAW_SQL_ONLY_TABLES (scripts/check-schema-drift.ts)'); }); it("does not flag a migrated table that is on the raw-SQL-only allowlist", () => { @@ -135,7 +135,7 @@ describe("check-schema-drift script (#2565)", () => { // without needing to fake-import a schema module through the CLI entrypoint. writeFileSync(join(dir, "0001_stub.sql"), "CREATE TABLE stub_only (id INTEGER PRIMARY KEY);\n"); try { - execFileSync(TSX_BIN, ["scripts/check-schema-drift.mjs"], { + execFileSync(TSX_BIN, ["scripts/check-schema-drift.ts"], { encoding: "utf8", env: { ...process.env, CHECK_SCHEMA_DRIFT_DIR: dir }, }); diff --git a/test/unit/check-stuck-required-checks-script.test.ts b/test/unit/check-stuck-required-checks-script.test.ts index b9381f3714..1171008bc4 100644 --- a/test/unit/check-stuck-required-checks-script.test.ts +++ b/test/unit/check-stuck-required-checks-script.test.ts @@ -10,7 +10,7 @@ import { findStuckChecksForPr, minutesSince, runStuckCheckWatchdog, -} from "../../scripts/check-stuck-required-checks.mjs"; +} from "../../scripts/check-stuck-required-checks.js"; // #7455: findStuckChecksForPr's stuck/threshold decision and the watchdog's dry-run + marker-idempotency // only ran inside the un-guarded live-GitHub driver. With githubApi injected and the driver behind an diff --git a/test/unit/check-ui-mcp-version-copy-script.test.ts b/test/unit/check-ui-mcp-version-copy-script.test.ts index e1dd684fbd..da45ce47d5 100644 --- a/test/unit/check-ui-mcp-version-copy-script.test.ts +++ b/test/unit/check-ui-mcp-version-copy-script.test.ts @@ -13,10 +13,10 @@ import { SCAN_TARGETS, SOURCE_LATEST_PATH, writeKnownLatestVersion, -} from "../../scripts/check-ui-mcp-version-copy.mjs"; +} from "../../scripts/check-ui-mcp-version-copy.js"; const root = process.cwd(); -const SCRIPT_PATH = "scripts/check-ui-mcp-version-copy.mjs"; +const SCRIPT_PATH = "scripts/check-ui-mcp-version-copy.ts"; const sourceText = readFileSync(join(root, SOURCE_LATEST_PATH), "utf8"); function declaredConstant(name: string): string { @@ -157,7 +157,7 @@ describe("check-ui-mcp-version-copy script (#6292)", () => { it("passes cleanly against the real repo docs with the registry check stubbed offline", () => { const knownLatest = declaredConstant("MCP_PACKAGE_KNOWN_LATEST_VERSION"); - const out = execFileSync(process.execPath, [SCRIPT_PATH], { + const out = execFileSync(process.execPath, ["--experimental-strip-types", SCRIPT_PATH], { encoding: "utf8", env: { ...process.env, LOOPOVER_MCP_LATEST_VERSION: knownLatest }, }); @@ -232,7 +232,7 @@ describe("check-ui-mcp-version-copy script (#6292)", () => { tempDir = seedTempRepo("0.6.0"); const out = execFileSync( process.execPath, - [join(process.cwd(), SCRIPT_PATH), "--write"], + ["--experimental-strip-types", join(process.cwd(), SCRIPT_PATH), "--write"], { encoding: "utf8", cwd: tempDir, env: { ...process.env, LOOPOVER_MCP_LATEST_VERSION: "0.9.0" } }, ); expect(out).toContain("updated known latest 0.6.0 -> 0.9.0"); @@ -244,7 +244,7 @@ describe("check-ui-mcp-version-copy script (#6292)", () => { tempDir = seedTempRepo("0.9.0"); const out = execFileSync( process.execPath, - [join(process.cwd(), SCRIPT_PATH), "--write"], + ["--experimental-strip-types", join(process.cwd(), SCRIPT_PATH), "--write"], { encoding: "utf8", cwd: tempDir, env: { ...process.env, LOOPOVER_MCP_LATEST_VERSION: "0.9.0" } }, ); expect(out).not.toContain("updated known latest"); diff --git a/test/unit/ci-cf-typegen-check.test.ts b/test/unit/ci-cf-typegen-check.test.ts index fa222e765d..af9c3a60a3 100644 --- a/test/unit/ci-cf-typegen-check.test.ts +++ b/test/unit/ci-cf-typegen-check.test.ts @@ -30,7 +30,7 @@ describe("cf-typegen staleness guard (#2557)", () => { const pkg = record(JSON.parse(readFileSync("package.json", "utf8")), "package.json"); const scripts = record(pkg.scripts, "package.json.scripts"); - expect(scripts["cf-typegen:check"]).toBe("node scripts/gen-cf-typegen.mjs --check"); + expect(scripts["cf-typegen:check"]).toBe("node --experimental-strip-types scripts/gen-cf-typegen.ts --check"); expect(String(scripts["test:ci"])).toContain("npm run cf-typegen:check"); // Must run before typecheck, mirroring db:migrations:check's position -- a drift-check failure should // surface before the more expensive type/test/build steps run. diff --git a/test/unit/ci-cf-typegen.test.ts b/test/unit/ci-cf-typegen.test.ts index 597784a8b1..34e080f9b5 100644 --- a/test/unit/ci-cf-typegen.test.ts +++ b/test/unit/ci-cf-typegen.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { formatCfTypegenOutput, resolveLocalWranglerBin } from "../../scripts/gen-cf-typegen.mjs"; +import { formatCfTypegenOutput, resolveLocalWranglerBin } from "../../scripts/gen-cf-typegen.js"; // #1667-followup: wrangler's own `wrangler types` output packs every declared env var into ONE long // single-line `Pick>` union. Two independent PRs that each add a DIFFERENT diff --git a/test/unit/ci-duration-report-script.test.ts b/test/unit/ci-duration-report-script.test.ts index 84d4aaf3b5..fe12863048 100644 --- a/test/unit/ci-duration-report-script.test.ts +++ b/test/unit/ci-duration-report-script.test.ts @@ -4,9 +4,9 @@ import { durationSeconds, percentile, summarize, -} from "../../scripts/ci-duration-report.mjs"; +} from "../../scripts/ci-duration-report.js"; -// #7456: percentile/summarize/durationSeconds are the non-obvious pure logic in ci-duration-report.mjs +// #7456: percentile/summarize/durationSeconds are the non-obvious pure logic in ci-duration-report.ts // (a specific nearest-rank percentile with a floor clamp; a summarize() that excludes `cancelled` runs from // both the duration set and the failure-rate denominator, while treating `skipped` as a success). Importing // the module must not trigger its live-fetch driver -- the entrypoint guard now keeps that behind `main()`, diff --git a/test/unit/ci-extension-packages.test.ts b/test/unit/ci-extension-packages.test.ts index eff1df001c..d872ff9688 100644 --- a/test/unit/ci-extension-packages.test.ts +++ b/test/unit/ci-extension-packages.test.ts @@ -10,8 +10,8 @@ describe("browser extension workspace packages (#4866)", () => { expect(maintainer.name).toBe("@loopover/extension"); expect(miner.name).toBe("@loopover/miner-extension"); - expect(maintainer.scripts.build).toContain("build-extension.mjs"); - expect(miner.scripts.build).toContain("build-miner-extension.mjs"); + expect(maintainer.scripts.build).toContain("build-extension.ts"); + expect(miner.scripts.build).toContain("build-miner-extension.ts"); expect(miner.scripts.lint).toContain("node --check"); expect(miner.scripts.typecheck).toBe("npm run lint"); }); @@ -26,7 +26,7 @@ describe("browser extension workspace packages (#4866)", () => { it("includes both extensions in ci.yml's ui path filter and validate-code steps", () => { const workflow = read(".github/workflows/ci.yml"); expect(workflow).toContain("apps/loopover-miner-extension/**"); - expect(workflow).toContain("scripts/build-miner-extension.mjs"); + expect(workflow).toContain("scripts/build-miner-extension.ts"); expect(workflow).toContain("name: Extension lint"); // Routed through Turborepo (turbo.json's @loopover/extension#lint/typecheck and // @loopover/miner-extension#lint/typecheck) -- see ci.yml's comment on these steps. diff --git a/test/unit/ci-openapi-settings-parity.test.ts b/test/unit/ci-openapi-settings-parity.test.ts index 497834ebc7..3c38d38200 100644 --- a/test/unit/ci-openapi-settings-parity.test.ts +++ b/test/unit/ci-openapi-settings-parity.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { diffFieldSets, extractRepoSettingsPreviewFieldNames, extractRepositorySettingsFieldNames, SETTINGS_PREVIEW_PATH, TYPES_PATH } from "../../scripts/check-openapi-settings-parity.mjs"; +import { diffFieldSets, extractRepoSettingsPreviewFieldNames, extractRepositorySettingsFieldNames, SETTINGS_PREVIEW_PATH, TYPES_PATH } from "../../scripts/check-openapi-settings-parity.js"; import { RepoSettingsPreviewSchema, RepositorySettingsSchema } from "../../src/openapi/schemas"; // #2556: RepositorySettingsSchema (hand-authored Zod) can silently drift from the RepositorySettings TS diff --git a/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts b/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts index fe00e1a9ad..ba45ab8d01 100644 --- a/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts +++ b/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; // Drift guard (#1943 gate review finding): the self-hosting troubleshooting runbooks reference exact // Prometheus metric names and alert names. If a metric is ever renamed/removed in src/, or an alert is // renamed/removed in prometheus/rules/alerts.yml, this test fails instead of the docs silently going stale -// — mirrors the same source-of-truth-diff approach as scripts/check-openapi-settings-parity.mjs (#2556). +// — mirrors the same source-of-truth-diff approach as scripts/check-openapi-settings-parity.ts (#2556). const DOC_PATH = "apps/loopover-ui/content/docs/self-hosting-troubleshooting.mdx"; const doc = readFileSync(DOC_PATH, "utf8"); diff --git a/test/unit/export-d1-core.test.ts b/test/unit/export-d1-core.test.ts index c05420c906..0a0e243479 100644 --- a/test/unit/export-d1-core.test.ts +++ b/test/unit/export-d1-core.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildExportManifest, buildTableExport, checksumRows, EXCLUDED_TABLES, filterRowsSince, isSafeTableName, redactRow, REDACTED_COLUMNS } from "../../scripts/export-d1-core.mjs"; +import { buildExportManifest, buildTableExport, checksumRows, EXCLUDED_TABLES, filterRowsSince, isSafeTableName, redactRow, REDACTED_COLUMNS } from "../../scripts/export-d1-core.js"; describe("export-d1-core isSafeTableName (SQL-injection guard)", () => { it("accepts plain SQL identifiers", () => { diff --git a/test/unit/fetch-test-timing-script.test.ts b/test/unit/fetch-test-timing-script.test.ts index 8d22f35bac..20e05b7072 100644 --- a/test/unit/fetch-test-timing-script.test.ts +++ b/test/unit/fetch-test-timing-script.test.ts @@ -3,9 +3,9 @@ import { aggregateByFile, RETRYABLE_STATUS, shouldRetryCodecovFetch, -} from "../../scripts/fetch-test-timing.mjs"; +} from "../../scripts/fetch-test-timing.js"; -describe("fetch-test-timing.mjs (#7457)", () => { +describe("fetch-test-timing.ts (#7457)", () => { describe("aggregateByFile", () => { it("sums multiple rows for the same file within one commit before averaging across commits", () => { // Two test cases in commit A (2s + 3s = 5s that run), one case in commit B (1s). Average across diff --git a/test/unit/forbidden-content.test.ts b/test/unit/forbidden-content.test.ts index d518b8c896..959a66680b 100644 --- a/test/unit/forbidden-content.test.ts +++ b/test/unit/forbidden-content.test.ts @@ -1,9 +1,10 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.mjs"; +import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.js"; -// forbidden-content.mjs calls itself the single source of truth for the packaged secret-shape detector, but +// forbidden-content.ts calls itself the single source of truth for the packaged secret-shape detector, but // nothing enforced it: check-mcp-package.mjs re-declared the regex as its own local constant and the two could // drift apart unnoticed (#6290). These assertions pin both halves of the claim -- the structural one (each // checker imports the constant rather than owning a copy) and the behavioral one (each checker actually rejects @@ -23,7 +24,11 @@ const SECRET_SHAPED_PROBE = ["PROBE", "_", "SECRET", "=", "value"].join(""); // Run a checker as a subprocess (never import it): both scripts run `npm pack` at import time, and neither has a // .d.mts, so importing them from TS would also break the typecheck gate. Their env seams let a single file drive -// the whole file list + content. +// the whole file list + content. Run via tsx, not plain node: both scripts import forbidden-content.ts (and +// check-mcp-package.mjs also imports mcp-package-allowlist.ts) directly, so plain node can't resolve those +// local .ts imports. +const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); + function runChecker( checker: string, files: string[], @@ -36,7 +41,7 @@ function runChecker( [isMiner ? "CHECK_MINER_PACK_TEST_CONTENT" : "CHECK_MCP_PACK_TEST_CONTENT"]: content, }; try { - return { status: 0, out: execFileSync(process.execPath, [checker], { encoding: "utf8", env }) }; + return { status: 0, out: execFileSync(TSX_BIN, [checker], { encoding: "utf8", env }) }; } catch (err) { const e = err as { status?: number; stdout?: string; stderr?: string }; return { status: e.status ?? 1, out: `${e.stdout ?? ""}${e.stderr ?? ""}` }; @@ -46,7 +51,7 @@ function runChecker( describe("FORBIDDEN_CONTENT is the single source of truth (#6290)", () => { it.each(PACKAGE_CHECKERS)("%s imports the shared constant instead of re-declaring it", (checker) => { const source = readFileSync(checker, "utf8"); - expect(source).toContain('import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs";'); + expect(source).toContain('import { FORBIDDEN_CONTENT } from "./forbidden-content.js";'); expect(source).toContain("FORBIDDEN_CONTENT.test("); // The drift this guards against: a checker owning its own copy of the detector. expect(source).not.toMatch(/const\s+FORBIDDEN_CONTENT\s*=/); diff --git a/test/unit/gen-command-reference-script.test.ts b/test/unit/gen-command-reference-script.test.ts index 40496e5806..3a8b234465 100644 --- a/test/unit/gen-command-reference-script.test.ts +++ b/test/unit/gen-command-reference-script.test.ts @@ -10,7 +10,7 @@ import { extractCatalogEntries, renderCommandList, renderCommandReferenceModule, -} from "../../scripts/gen-command-reference.mjs"; +} from "../../scripts/gen-command-reference.js"; function fixtureRoot(commandsSource: string): string { const root = mkdtempSync(join(tmpdir(), "gt-command-reference-")); @@ -135,7 +135,7 @@ describe("gen-command-reference script (#3046)", () => { actionCommands: [{ id: "review", title: "Review", description: "Request review." }], }); - expect(output).toContain("// Generated by scripts/gen-command-reference.mjs. Do not edit manually."); + expect(output).toContain("// Generated by scripts/gen-command-reference.ts. Do not edit manually."); expect(output).toContain("npm run command-reference"); expect(output).toContain('export const PUBLIC_COMMAND_LIST =\n "@loopover help";'); expect(output).toContain('export const MAINTAINER_COMMAND_LIST =\n "@loopover queue-summary";'); @@ -160,7 +160,7 @@ describe("gen-command-reference script (#3046)", () => { }); it("prints a clean summary and exits 0 for the real repo state when run as a subprocess with --check", () => { - const output = execFileSync("node", ["scripts/gen-command-reference.mjs", "--check"], { encoding: "utf8" }); + const output = execFileSync("node", ["--experimental-strip-types", "scripts/gen-command-reference.ts", "--check"], { encoding: "utf8" }); expect(output).toMatch(/gen-command-reference: checked \d+ public \+ \d+ maintainer-only \+ \d+ action command references in/); }); diff --git a/test/unit/generate-mcp-changelog-script.test.ts b/test/unit/generate-mcp-changelog-script.test.ts index eb948d1bd6..80c5091a83 100644 --- a/test/unit/generate-mcp-changelog-script.test.ts +++ b/test/unit/generate-mcp-changelog-script.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; // #7460: readReleasePrepEntries()/dependencyChange()/readConstant() synthesize changelog entries for // release-prep-only changes (dependency bumps, compatibility-constant updates) that real commit-message // parsing would never surface -- these three functions had no coverage at all. Mirrors -// test/unit/mcp-release.test.ts's pattern for the already-tested sibling logic in mcp-release-core.mjs. +// test/unit/mcp-release.test.ts's pattern for the already-tested sibling logic in mcp-release-core.ts. const MCP_PACKAGE_JSON = "packages/loopover-mcp/package.json"; const ROOT_PACKAGE_JSON = "package.json"; @@ -56,7 +56,7 @@ function setFixture(current: FileMap, previous: FileMap) { }); } -const { readReleasePrepEntries, dependencyChange, readConstant } = await import("../../scripts/generate-mcp-changelog.mjs"); +const { readReleasePrepEntries, dependencyChange, readConstant } = await import("../../scripts/generate-mcp-changelog.js"); describe("readConstant (#7460)", () => { it("extracts a matching exported string constant", () => { diff --git a/test/unit/gittensor-impact-card-script.test.ts b/test/unit/gittensor-impact-card-script.test.ts index 1080bd0ad2..885c1f38e0 100644 --- a/test/unit/gittensor-impact-card-script.test.ts +++ b/test/unit/gittensor-impact-card-script.test.ts @@ -3,9 +3,9 @@ import { fetchGtLogoSvg, fetchJson, GITTENSOR_IMPACT_CARD_FETCH_TIMEOUT_MS, -} from "../../scripts/gittensor-impact-card.mjs"; +} from "../../scripts/gittensor-impact-card.js"; -describe("gittensor-impact-card.mjs (#7231)", () => { +describe("gittensor-impact-card.ts (#7231)", () => { afterEach(() => { vi.restoreAllMocks(); }); diff --git a/test/unit/lint-composite-actions-script.test.ts b/test/unit/lint-composite-actions-script.test.ts index 481407bf22..63c1dcc1e3 100644 --- a/test/unit/lint-composite-actions-script.test.ts +++ b/test/unit/lint-composite-actions-script.test.ts @@ -9,7 +9,7 @@ import { runLint, validateActionFile, type Dirent, -} from "../../scripts/lint-composite-actions.mjs"; +} from "../../scripts/lint-composite-actions.js"; // #7459: findActionFiles, the schema check, and the shell:-presence check only ran inside the // disk-reading/process.exit driver. With readdir/readFile/validateSchema injected and the driver behind diff --git a/test/unit/load-test-worker-script.test.ts b/test/unit/load-test-worker-script.test.ts index 1f1b3ecb3f..49bf0426d3 100644 --- a/test/unit/load-test-worker-script.test.ts +++ b/test/unit/load-test-worker-script.test.ts @@ -11,7 +11,7 @@ import { requestOnce, runConcurrencyLevel, runLoadTest, -} from "../../scripts/load-test-worker.mjs"; +} from "../../scripts/load-test-worker.js"; describe("worker load-test script (#4913)", () => { afterEach(() => { @@ -178,7 +178,8 @@ describe("worker load-test script (#4913)", () => { const result = spawnSync( process.execPath, [ - "scripts/load-test-worker.mjs", + "--experimental-strip-types", + "scripts/load-test-worker.ts", "--origin", "http://127.0.0.1:1", "--levels", diff --git a/test/unit/mcp-cli-doctor.test.ts b/test/unit/mcp-cli-doctor.test.ts index e03bef097b..d7fd54ae70 100644 --- a/test/unit/mcp-cli-doctor.test.ts +++ b/test/unit/mcp-cli-doctor.test.ts @@ -55,12 +55,22 @@ describe("loopover-mcp CLI — doctor", () => { expect.arrayContaining([ expect.objectContaining({ id: "auth", title: "Auth", status: "pass" }), expect.objectContaining({ id: "api_compatibility", title: "API compatibility", status: "pass" }), - expect.objectContaining({ id: "local_repo_readiness", title: "Local repo readiness", status: "pass" }), + // Not asserting status here: this group's own "client_path" sub-check (findExecutable("loopover-mcp")) + // does a plain PATH scan for a "loopover-mcp" executable, which resolves via node_modules/.bin's npm- + // workspace bin-link -- but npm only creates that symlink if bin/loopover-mcp.js already exists at + // `npm ci` time. Since compiled output is no longer committed (build(mcp,miner): stop committing + // compiled .js/.d.ts entirely), every CI job's npm ci runs before any build step, so the symlink is + // never created there, regardless of a later build -- this group's status is genuinely "warn" in that + // environment and "pass" only where something else (a real global install, e.g.) already put + // loopover-mcp on PATH. Checked explicitly below instead, tolerating either. + expect.objectContaining({ id: "local_repo_readiness", title: "Local repo readiness" }), expect.objectContaining({ id: "scorer_availability", title: "Scorer availability", status: "warn" }), expect.objectContaining({ id: "output_safety", title: "Output safety", status: "pass" }), expect.objectContaining({ id: "next_command", title: "Next command", status: "warn" }), ]), ); + const localRepoReadiness = payload.checklist.find((group) => group.id === "local_repo_readiness"); + expect(["pass", "warn"]).toContain(localRepoReadiness?.status); expect(payload.nextCommand).toMatchObject({ command: "loopover-mcp doctor --json", reason: expect.stringContaining("local scorer"), diff --git a/test/unit/mcp-release-candidate.test.ts b/test/unit/mcp-release-candidate.test.ts index a3d49dd292..75a2523b33 100644 --- a/test/unit/mcp-release-candidate.test.ts +++ b/test/unit/mcp-release-candidate.test.ts @@ -12,7 +12,7 @@ import { parseReleaseTag, redactSensitive, unexpectedTarballFiles, -} from "../../scripts/mcp-release-candidate-core.mjs"; +} from "../../scripts/mcp-release-candidate-core.js"; const FORBIDDEN_PUBLIC_LANGUAGE = /\b(wallet|hotkey|coldkey|raw trust|trust score|payout|reward estimate|farming|private reviewability|public score estimate)\b/i; diff --git a/test/unit/mcp-release.test.ts b/test/unit/mcp-release.test.ts index 9a6e2e139a..42636de724 100644 --- a/test/unit/mcp-release.test.ts +++ b/test/unit/mcp-release.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { closeResolvedIssueIfPresent, isReleaseWatchIssue } from "../../scripts/check-mcp-release-due.mjs"; -import { buildMcpReleaseIssue, buildMcpReleaseReport, renderMcpChangelog, selectMcpReleaseCommits } from "../../scripts/mcp-release-core.mjs"; +import { closeResolvedIssueIfPresent, isReleaseWatchIssue } from "../../scripts/check-mcp-release-due.js"; +import { buildMcpReleaseIssue, buildMcpReleaseReport, renderMcpChangelog, selectMcpReleaseCommits } from "../../scripts/mcp-release-core.js"; type TestCommit = { sha: string; diff --git a/test/unit/mcp-typescript-migration-complete.test.ts b/test/unit/mcp-typescript-migration-complete.test.ts deleted file mode 100644 index df77f896d6..0000000000 --- a/test/unit/mcp-typescript-migration-complete.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { readdirSync } from "node:fs"; -import { join } from "node:path"; -import { describe, expect, it } from "vitest"; - -const mcpRoot = join(process.cwd(), "packages/loopover-mcp"); -const dirs = ["bin", "lib"] as const; - -function listBasenames(dir: string, predicate: (name: string) => boolean): string[] { - return readdirSync(join(mcpRoot, dir)) - .filter(predicate) - .map((name) => name.replace(/\.(?:d\.ts|ts|js)$/, "")) - .sort(); -} - -/** - * #7291 closing guard for the phased #7328/#7329/#7330 mcp TypeScript migration -- the loopover-mcp - * twin of loopover-miner's own #7290/#7317 guard in test/unit/miner-typescript-migration-complete.test.ts. - * Every runtime file under packages/loopover-mcp/{bin,lib} must be compiler-owned (.ts source -> emitted - * .js). A lone hand-maintained .js is the drift gap the migration was filed to close. - */ -describe("loopover-mcp TypeScript migration complete (#7291)", () => { - it("has zero hand-maintained .js orphans — every basename has a real .ts source", () => { - for (const dir of dirs) { - const sources = new Set(listBasenames(dir, (name) => name.endsWith(".ts") && !name.endsWith(".d.ts"))); - const scripts = listBasenames(dir, (name) => name.endsWith(".js")); - - const jsWithoutTs = scripts.filter((base) => !sources.has(base)); - - expect(jsWithoutTs, `${dir}/ .js without sibling .ts`).toEqual([]); - expect(scripts).toEqual([...sources].sort()); - } - }); - - // Unlike loopover-miner, packages/loopover-mcp/tsconfig.json sets declaration: false (no pre-existing - // hand-written .d.ts files to replace when the mcp migration started) -- so a .d.ts appearing here at - // all, not just an orphaned one, means either that tsconfig setting or this guard has drifted. - it("emits no .d.ts (declaration: false in packages/loopover-mcp/tsconfig.json)", () => { - for (const dir of dirs) { - const declarations = readdirSync(join(mcpRoot, dir)).filter((name) => name.endsWith(".d.ts")); - expect(declarations, `${dir}/ unexpected .d.ts`).toEqual([]); - } - }); -}); diff --git a/test/unit/migration-0102-linked-issue-gate-mode.test.ts b/test/unit/migration-0102-linked-issue-gate-mode.test.ts index e619d089b7..27c562eac1 100644 --- a/test/unit/migration-0102-linked-issue-gate-mode.test.ts +++ b/test/unit/migration-0102-linked-issue-gate-mode.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; const MIGRATION_FILE = "0102_fix_linked_issue_gate_mode_default.sql"; // Replays every migrations/*.sql file BEFORE 0102 into a fresh in-memory DB (mirrors -// scripts/check-schema-drift.mjs's own "replay migrations into node:sqlite" approach), so the table shape +// scripts/check-schema-drift.ts's own "replay migrations into node:sqlite" approach), so the table shape // this test inserts into is exactly what migration 0102 itself was written against -- not a guess. The // TestD1Database helper (test/helpers/d1.ts) can't be reused here: it concatenates and applies EVERY // migration (including 0102) up front, so the `repository_settings` table would already be empty-and-fixed diff --git a/test/unit/miner-mcp-contract.test.ts b/test/unit/miner-mcp-contract.test.ts index 9853e2da5b..154ab29f91 100644 --- a/test/unit/miner-mcp-contract.test.ts +++ b/test/unit/miner-mcp-contract.test.ts @@ -11,7 +11,7 @@ import { import { initGovernorLedger } from "../../packages/loopover-miner/lib/governor-ledger.js"; // The SAME secret-shape matcher the miner pack validator uses — imported from its single source of truth (rather // than hand-duplicated here) so the two stay byte-for-byte in sync instead of relying on manual vigilance. -import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.mjs"; +import { FORBIDDEN_CONTENT } from "../../scripts/forbidden-content.js"; // Shared contract/parity suite (#5199) across every read-only AMS MCP tool — mirrors the spirit of the engine's // driver-parity suite (#4296). One parameterized table enforces, for ALL tools at once, the invariants that diff --git a/test/unit/miner-typescript-migration-complete.test.ts b/test/unit/miner-typescript-migration-complete.test.ts deleted file mode 100644 index 7b020c941d..0000000000 --- a/test/unit/miner-typescript-migration-complete.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { readdirSync } from "node:fs"; -import { join } from "node:path"; -import { describe, expect, it } from "vitest"; - -const minerRoot = join(process.cwd(), "packages/loopover-miner"); -const dirs = ["bin", "lib"] as const; - -function listBasenames(dir: string, predicate: (name: string) => boolean): string[] { - return readdirSync(join(minerRoot, dir)) - .filter(predicate) - .map((name) => name.replace(/\.(?:d\.ts|ts|js)$/, "")) - .sort(); -} - -/** - * #7317 closing guard for the #7290 miner TypeScript migration: every runtime file under - * packages/loopover-miner/{bin,lib} must be compiler-owned (.ts source → emitted .js + .d.ts). - * A lone hand-maintained .js/.d.ts pair is the drift gap the migration was filed to close. - */ -describe("loopover-miner TypeScript migration complete (#7317)", () => { - it("has zero hand-maintained .js/.d.ts orphans — every basename has a real .ts source", () => { - for (const dir of dirs) { - const sources = new Set(listBasenames(dir, (name) => name.endsWith(".ts") && !name.endsWith(".d.ts"))); - const scripts = listBasenames(dir, (name) => name.endsWith(".js")); - const declarations = listBasenames(dir, (name) => name.endsWith(".d.ts")); - - const jsWithoutTs = scripts.filter((base) => !sources.has(base)); - const dtsWithoutTs = declarations.filter((base) => !sources.has(base)); - - expect(jsWithoutTs, `${dir}/ .js without sibling .ts`).toEqual([]); - expect(dtsWithoutTs, `${dir}/ .d.ts without sibling .ts`).toEqual([]); - // Emitted declarations track sources 1:1 (tsc declaration:true in-place emit). - expect(declarations).toEqual([...sources].sort()); - expect(scripts).toEqual([...sources].sort()); - } - }); -}); diff --git a/test/unit/observability-ci.test.ts b/test/unit/observability-ci.test.ts index c973f8ce84..1d21e196c9 100644 --- a/test/unit/observability-ci.test.ts +++ b/test/unit/observability-ci.test.ts @@ -50,12 +50,12 @@ describe("observability config CI guard", () => { expect(setupWorkspaceStep).toBeDefined(); expect(setupWorkspaceStep!.uses).toBe("./.github/actions/setup-workspace"); expect(validateStep).toBeDefined(); - // Not gated on `backend`: scripts/validate-observability-configs.mjs only ever reads + // Not gated on `backend`: scripts/validate-observability-configs.ts only ever reads // grafana/dashboards/*.json and prometheus/rules/alerts.yml, both fully covered by the // `observability` filter checked above -- a `backend` clause had no structural justification and ran // this on every backend PR (the highest-volume PR shape in the repo) for nothing. expect(String(validateStep!.if)).toBe("${{ github.event_name == 'push' || needs.changes.outputs.observability == 'true' }}"); expect(record(validateStep!.env, "validateStep.env").NODE_OPTIONS).toBe(""); - expect(validateStep!.run).toBe("node scripts/validate-observability-configs.mjs"); + expect(validateStep!.run).toBe("node --experimental-strip-types scripts/validate-observability-configs.ts"); }); }); diff --git a/test/unit/observability-release-fetch-timeout.test.ts b/test/unit/observability-release-fetch-timeout.test.ts index cc6e4b3cd4..83fdb9636e 100644 --- a/test/unit/observability-release-fetch-timeout.test.ts +++ b/test/unit/observability-release-fetch-timeout.test.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest"; // container/GitHub endpoints that a unit test can't stand up. it("check-mcp-release-due's githubRequest fetch carries an AbortSignal timeout", () => { - const src = readFileSync("scripts/check-mcp-release-due.mjs", "utf8"); + const src = readFileSync("scripts/check-mcp-release-due.ts", "utf8"); const fetchCount = (src.match(/\bfetch\(/g) ?? []).length; const timeoutCount = (src.match(/AbortSignal\.timeout\(/g) ?? []).length; expect(fetchCount).toBe(1); diff --git a/test/unit/orb-release.test.ts b/test/unit/orb-release.test.ts index c0fe229626..ab016f1038 100644 --- a/test/unit/orb-release.test.ts +++ b/test/unit/orb-release.test.ts @@ -11,7 +11,7 @@ import { parseOrbBetaVersion, parseSemver, selectImageRelevantCommits, -} from "../../scripts/orb-release-core.mjs"; +} from "../../scripts/orb-release-core.js"; type TestCommit = { sha: string; subject: string; body?: string; files: string[] }; diff --git a/test/unit/selfhost-env-reference-script.test.ts b/test/unit/selfhost-env-reference-script.test.ts index 47a935844f..6423c42be7 100644 --- a/test/unit/selfhost-env-reference-script.test.ts +++ b/test/unit/selfhost-env-reference-script.test.ts @@ -6,7 +6,7 @@ import { collectSelfHostEnvVars, renderSelfHostEnvReferenceMarkdown, writeSelfHostEnvReference, -} from "../../scripts/gen-selfhost-env-reference.mjs"; +} from "../../scripts/gen-selfhost-env-reference.js"; function fixtureRoot(): string { const root = mkdtempSync(join(tmpdir(), "gt-env-reference-")); diff --git a/test/unit/smoke-production-versions.test.ts b/test/unit/smoke-production-versions.test.ts index 23f9ee0e37..78ddfd0c57 100644 --- a/test/unit/smoke-production-versions.test.ts +++ b/test/unit/smoke-production-versions.test.ts @@ -6,7 +6,7 @@ import { expectedMcpVersions, loadMcpPackageJson, MINIMUM_SUPPORTED_MCP_VERSION, -} from "../../scripts/smoke-production-versions.mjs"; +} from "../../scripts/smoke-production-versions.js"; describe("smoke-production-versions (#6293)", () => { it("derives latest from the MCP package.json and keeps the supported floor fixed", () => { diff --git a/test/unit/support/mcp-cli-harness.ts b/test/unit/support/mcp-cli-harness.ts index 792adf4199..2f26c988c5 100644 --- a/test/unit/support/mcp-cli-harness.ts +++ b/test/unit/support/mcp-cli-harness.ts @@ -5,7 +5,20 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect } from "vitest"; -export const bin = join(process.cwd(), "packages/loopover-mcp/bin/loopover-mcp.js"); +// 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"); export const repoOnboardingPackFixture = { repoFullName: "owner/repo", accepted: true, @@ -43,7 +56,7 @@ export async function closeFixtureServer() { export function run(args: string[], env: Record = {}) { try { - return execFileSync("node", [bin, ...args], { + return execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -65,8 +78,8 @@ export function run(args: string[], env: Record = {}) { export function runAsync(args: string[], env: Record = {}) { return new Promise((resolve, reject) => { execFile( - "node", - [bin, ...args], + process.execPath, + [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { @@ -91,7 +104,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("node", [bin, ...args], { + execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -105,7 +118,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 ${bin} ${args.join(" ")}\` to fail`); + throw new Error(`expected \`node --experimental-strip-types ${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 c06b242fd3..35be9f321c 100644 --- a/test/unit/support/miner-cli-harness.ts +++ b/test/unit/support/miner-cli-harness.ts @@ -19,9 +19,22 @@ 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"]; export const bin = join( process.cwd(), - "packages/loopover-miner/bin/loopover-miner.js", + "packages/loopover-miner/bin/loopover-miner.ts", ); let server: Server | null = null; @@ -32,7 +45,7 @@ export async function closeFixtureServer() { } export function run(args: string[], env: Record = {}) { - return execFileSync("node", [bin, ...args], { + return execFileSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -50,7 +63,7 @@ export function runCliResult( args: string[], env: Record = {}, ): CliProcessResult { - const result = spawnSync("node", [bin, ...args], { + const result = spawnSync(process.execPath, [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { ...process.env, @@ -70,8 +83,8 @@ export function runCliResult( export function runAsync(args: string[], env: Record = {}) { return new Promise((resolve) => { execFile( - "node", - [bin, ...args], + process.execPath, + [...NODE_STRIP_TYPES_ARGS, bin, ...args], { encoding: "utf8", env: { diff --git a/test/unit/sync-release-lockfile-versions.test.ts b/test/unit/sync-release-lockfile-versions.test.ts index d6c388b458..9299f4bc5e 100644 --- a/test/unit/sync-release-lockfile-versions.test.ts +++ b/test/unit/sync-release-lockfile-versions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { main, syncLockfileVersions } from "../../scripts/sync-release-lockfile-versions.mjs"; +import { main, syncLockfileVersions } from "../../scripts/sync-release-lockfile-versions.js"; const SAMPLE_LOCK = `{ "name": "loopover", diff --git a/test/unit/sync-release-manifest.test.ts b/test/unit/sync-release-manifest.test.ts index 0f5d19a388..6d6c978066 100644 --- a/test/unit/sync-release-manifest.test.ts +++ b/test/unit/sync-release-manifest.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { main, MANIFEST_PATH, syncManifestVersions } from "../../scripts/sync-release-manifest.mjs"; +import { main, MANIFEST_PATH, syncManifestVersions } from "../../scripts/sync-release-manifest.js"; const SAMPLE_MANIFEST = JSON.stringify( { diff --git a/test/unit/validate-observability-configs-script.test.ts b/test/unit/validate-observability-configs-script.test.ts index 0b92674ea5..cc74ede58f 100644 --- a/test/unit/validate-observability-configs-script.test.ts +++ b/test/unit/validate-observability-configs-script.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { validateAlertRules, validateDashboards } from "../../scripts/validate-observability-configs.mjs"; +import { validateAlertRules, validateDashboards } from "../../scripts/validate-observability-configs.js"; function tmpDashboardDir(files: Record): string { const dir = mkdtempSync(join(tmpdir(), "gt-dash-")); diff --git a/test/unit/validate-selfhost-sourcemap-script.test.ts b/test/unit/validate-selfhost-sourcemap-script.test.ts index bad6d37eae..2e1f7869f6 100644 --- a/test/unit/validate-selfhost-sourcemap-script.test.ts +++ b/test/unit/validate-selfhost-sourcemap-script.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { validateSourcemap } from "../../scripts/validate-selfhost-sourcemap.mjs"; +import { validateSourcemap } from "../../scripts/validate-selfhost-sourcemap.js"; const BUNDLE = "/tmp/dist/server.mjs"; const MAP = "/tmp/dist/server.mjs.map"; @@ -29,7 +29,7 @@ function harness(files: Record) { }; } -describe("validate-selfhost-sourcemap.mjs (#7458)", () => { +describe("validate-selfhost-sourcemap.ts (#7458)", () => { it("passes a well-formed minimal source map", () => { expect( validateSourcemap( diff --git a/turbo.json b/turbo.json index f1e2a25e99..0587bd154e 100644 --- a/turbo.json +++ b/turbo.json @@ -138,7 +138,7 @@ // apps/loopover-ui/public/downloads/loopover-extension.zip, ran `turbo run build --filter=@loopover/ui` // against a warm cache, the zip never came back -- a cache HIT replays logs but restores nothing when // no outputs are declared). This build's real output crosses a package boundary - // (apps/loopover-ui/public/downloads/loopover-extension.zip, written by scripts/build-extension.mjs), + // (apps/loopover-ui/public/downloads/loopover-extension.zip, written by scripts/build-extension.ts), // and Turborepo's docs are silent on whether cross-package "outputs" globs are honored on restore -- // rather than rely on undocumented behavior, this task is never cached at all, so it always actually // executes (cheap, a handful of files) whenever something depends on it. That's what actually closes @@ -161,7 +161,16 @@ }, "@loopover/mcp#build": { "dependsOn": ["^build"], - "outputs": [] + // Was `[]` (log-only caching, correct back when bin/lib's compiled .js was always committed -- + // present via git regardless of whether turbo actually ran tsc or replayed a cached log). Now that + // compiled output is gitignored (build(mcp,miner): stop committing compiled .js/.d.ts entirely), + // `[]` means a cache HIT never restores the real files turbo never snapshotted, only replays the + // log text -- confirmed live: validate-tests' 3 parallel shards share one turbo-tests- cache key, + // 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"] }, "@loopover/miner#build:tsc": { "dependsOn": ["^build"], diff --git a/vitest.config.ts b/vitest.config.ts index 851063f032..f8f45f5a19 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -35,35 +35,44 @@ 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", - // Files converted to real TypeScript (#7290 / #7317) execute as the compiled .js above, but - // v8's coverage provider remaps through the inline sourcemap tsc emits, attributing coverage to - // the .ts source instead -- this entry is what keeps that remapped file in the report. "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) is NOT ignore-listed in codecov.yml -- test/unit/ + // 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", "packages/loopover-miner/bin/**/*.ts", "packages/discovery-index/src/**/*.ts", - // packages/loopover-mcp/lib/*.js are plain JS today; issue #7291 migrates them to real TypeScript, - // keeping the same .js/.ts/.d.ts triplet shape packages/loopover-miner/lib/** already has (so - // coverage is wired BEFORE that PR lands, not as a follow-up fix). All 5 files - // (format-table/local-branch/redact-local-path/telemetry/cli-error) are now 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. + // 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.js (~6,600 of ~7,400 lines in the package) is tested + // 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). Same shape as - // packages/loopover-miner/bin/loopover-miner.ts above -- and, consistent with that file NOT getting - // a codecov.yml exemption (#4864), this isn't ignore-listed either: #7291 (the TS migration) will - // need real in-process tests or a testable-export refactor for this file, not a free pass. + // 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", "packages/loopover-mcp/bin/**/*.ts", // review-enrichment is a standalone (non-workspace) package with its own node:test suite; its @@ -105,23 +114,22 @@ export default defineConfig({ // merged shards instead; this backstop still runs on the full local // `npm run test:coverage`. Spread-omit the key (rather than set it to // undefined) to satisfy exactOptionalPropertyTypes. - // All four are set below 90 (not just branches) because of a reproducible v8 - // `--mergeReports` artifact, not a real coverage drop: merging a shard that - // never touches a given compiled-from-.ts miner lib file (packages/loopover- - // miner/lib/**/*.js, remapped to .ts via inline sourcemap, #7290) with a - // shard that does can REDUCE that file's reported hit ratio -- on ANY of the - // four metrics -- below what the exercising shard alone reported. Confirmed - // with an isolated 2-shard --reporter=blob + --mergeReports repro on a single - // file (a file at 99%+ branches in its own shard dropped to ~71% once merged - // with a shard that ran zero of its tests), unaffected by `coverage.all`. It - // does not reproduce on plain live-transformed .ts (src/**, packages/loopover- - // engine/src/**), only on this pre-compiled .ts-with-inline-sourcemap remap - // path, and grows as more packages/loopover-miner/lib files convert from .js - // to .ts (#7290 phase 2+) -- branches alone tripped first (PR #7351, widened - // to 85), then functions tripped next batch at 89.74% even with branches - // fixed, confirming this isn't a one-metric fluke. All four are widened - // together now, with real margin, so each further batch in this multi-phase - // migration doesn't need its own one-metric-at-a-time fix here. + // All four are set below 90 (not just branches), originally to route around a reproducible v8 + // `--mergeReports` artifact, not a real coverage drop: merging a shard that never touched a given + // compiled-from-.ts miner lib file (packages/loopover-miner/lib/**/*.js, remapped to .ts via inline + // sourcemap, #7290) with a shard that did could REDUCE that file's reported hit ratio -- on ANY of + // the four metrics -- below what the exercising shard alone reported (confirmed with an isolated + // 2-shard --reporter=blob + --mergeReports repro: a file at 99%+ branches in its own shard dropped + // to ~71% once merged with a shard that ran zero of its tests, unaffected by `coverage.all`). That + // never reproduced on plain live-transformed .ts (src/**, packages/loopover-engine/src/**), only on + // the pre-compiled .ts-with-inline-sourcemap remap path -- which no longer exists anywhere in this + // include list (packages/loopover-{miner,mcp} now execute their real .ts directly too, same as + // everything else here), so this specific artifact's precondition is gone. Left at 80 anyway rather + // than raised back toward 90: the widening predates the mergeReports fix by multiple rounds (PR + // #7351 widened branches to 85 for it; the next batch tripped functions at 89.74% even with branches + // already fixed) and the note two paragraphs up ("Do NOT raise these... or the cross-PR churn + // returns") describes a separate, still-live reason to stay low that this change doesn't touch -- + // revisiting the threshold itself is a deliberate follow-up decision, not a side effect of this one. // `npm run test:coverage` (unsharded, no merge) is unaffected and is the // faithful local signal; only CI's sharded validate-tests-merge job sees // this. Real per-line/branch enforcement is Codecov's codecov/patch gate