Skip to content

Commit b4e988e

Browse files
authored
Merge pull request #7218 from JSONbored/claude/open-issues-triage-a5f79f
feat(discovery-index): add the hosted discovery-index query service
2 parents 1b2a8cf + ba26336 commit b4e988e

26 files changed

Lines changed: 1955 additions & 4 deletions

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ LOOPOVER_REVIEW_ENRICHMENT=false
6666
# # available; otherwise GITHUB_PUBLIC_TOKEN).
6767
# REES_ANALYZERS=all # all | comma-list of exact names.
6868
# # Unknown names warn and are ignored; a typo-only list runs no analyzers.
69+
# --profile discovery-index (#4250, #7164): the optional hosted discovery-index service. Local orchestration
70+
# only -- public hosting/DNS/TLS is a separate, still-open decision (#7167), not part of this profile.
71+
# DISCOVERY_INDEX_SHARED_SECRET= # bearer secret required to call /v1/discovery-index/*; unset fails closed
72+
# DISCOVERY_INDEX_GITHUB_TOKEN= # this service's own GitHub token, isolated from REES's/the engine's
6973
# BEGIN GENERATED REES ANALYZERS
7074
# Current analyzer names:
7175
# dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
@@ -373,6 +377,7 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
373377
# POSTGRES_MEM_LIMIT=2g # --profile postgres / --profile pgbouncer
374378
# QDRANT_MEM_LIMIT=2g # --profile qdrant
375379
# REES_MEM_LIMIT=512m # --profile rees (the review-enrichment service)
380+
# DISCOVERY_INDEX_MEM_LIMIT=512m # --profile discovery-index (the hosted discovery-index service)
376381
# OLLAMA_MEM_LIMIT=20g # --profile ollama; raise this before pulling a large local model
377382
# # (default sized for an embed model + one vision model resident
378383
# # together, #4335 -- raise further for additional/larger models)

codecov.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ comment:
5757
require_changes: false
5858

5959
# Coverage is collected by vitest (v8) over src/**, packages/loopover-engine/src/**,
60-
# and packages/loopover-miner/lib/** (plus review-enrichment/src/** via the standalone
61-
# `rees` flag, c8 over its built dist remapped through source maps); mirror vitest's
60+
# packages/loopover-miner/lib/**, and packages/discovery-index/src/** (plus review-enrichment/src/** via
61+
# the standalone `rees` flag, c8 over its built dist remapped through source maps); mirror vitest's
6262
# exclusions here so the Codecov total matches the local report.
6363
ignore:
6464
- "src/env.d.ts"
@@ -74,3 +74,6 @@ ignore:
7474
# gated on PG_TEST_URL) + the real-PG boot. The dialect translation itself IS unit-tested (pg-dialect.ts).
7575
- "src/selfhost/pg-adapter.ts"
7676
- "src/selfhost/pg-queue.ts"
77+
# discovery-index's process entrypoint (#7164) -- same "Docker build+boot, not unit-coverable" reasoning
78+
# as src/server.ts above; app.ts (everything it wires together) is what tests actually import.
79+
- "packages/discovery-index/src/server.ts"

docker-compose.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# --profile workflows n8n workflow automation (Slack/Teams/email fan-out, scheduled reports)
2424
# --profile storage MinIO S3-compatible object store (Litestream destination, artifact blobs)
2525
# --profile caddy Caddy HTTPS terminator with auto-TLS (set DOMAIN= in .env)
26+
# --profile discovery-index Local orchestration for the hosted discovery-index service (#4250, #7164)
2627
# --profile observability Prometheus + Alertmanager + Loki + Promtail + Grafana (pre-wired)
2728
# --profile ams-observability Exports AMS (loopover-miner) attempt-log/prediction-ledger data to the
2829
# AMS Attempt Log / Prediction Ledger Grafana datasources
@@ -670,6 +671,47 @@ services:
670671
start_period: 20s
671672
retries: 3
672673

674+
# ── discovery-index (--profile discovery-index) ────────────────────────────
675+
# Local orchestration only, for testing against the hosted discovery-index service (#4250, #7164) before
676+
# it exists anywhere public -- the actual public hosting/DNS/TLS decision is separate (#7167), not this.
677+
# Unlike rees above, the build context is the repo root: this service depends on the @loopover/engine
678+
# workspace package, so its Dockerfile installs via npm workspaces rather than a self-contained `npm ci`.
679+
discovery-index:
680+
build:
681+
context: .
682+
dockerfile: packages/discovery-index/Dockerfile
683+
restart: unless-stopped
684+
<<: *default-logging
685+
profiles: ["discovery-index"]
686+
environment:
687+
PORT: "8080"
688+
# Soft defaults, not ":?required" -- compose interpolates every service's env vars for the whole file
689+
# even when --profile discovery-index isn't active (same reasoning as REES_SHARED_SECRET above). Unset
690+
# DISCOVERY_INDEX_SHARED_SECRET means the service fails closed (503) on every query call; unset
691+
# DISCOVERY_INDEX_GITHUB_TOKEN means /ready reports not-ready.
692+
DISCOVERY_INDEX_SHARED_SECRET: ${DISCOVERY_INDEX_SHARED_SECRET:-}
693+
DISCOVERY_INDEX_GITHUB_TOKEN: ${DISCOVERY_INDEX_GITHUB_TOKEN:-}
694+
# Never published to the host -- reached only over the internal docker network, same in-network-only
695+
# posture as rees above.
696+
expose:
697+
- "8080"
698+
deploy:
699+
resources:
700+
limits:
701+
memory: "${DISCOVERY_INDEX_MEM_LIMIT:-512m}"
702+
healthcheck:
703+
test:
704+
[
705+
"CMD",
706+
"node",
707+
"-e",
708+
"fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
709+
]
710+
interval: 30s
711+
timeout: 5s
712+
start_period: 20s
713+
retries: 3
714+
673715
# ── Litestream (--profile litestream) ─────────────────────────────────────
674716
# Continuous WAL backup of the SQLite DB to S3/B2/R2. Copy litestream.yml.example
675717
# → litestream.yml and fill in your bucket. Set LITESTREAM_* secrets in .env.

package-lock.json

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs",
100100
"test:smoke:browser:install": "playwright install chromium",
101101
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
102-
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-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",
102+
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run 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 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",
103103
"test:release": "npm run test:ci && npm run changelog:check",
104104
"test:release:mcp": "npm run test:ci",
105105
"test:watch": "vitest",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
*.tsbuildinfo
4+
.DS_Store
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# LoopOver discovery-index service (#4250, #7164). Standalone Hono/Node service, deployment shape mirrors
2+
# review-enrichment/ (own Dockerfile, own port, no Cloudflare Worker involvement). Build context =
3+
# the packages/discovery-index/ directory — but it depends on the @loopover/engine workspace package
4+
# (packages/loopover-engine), so the build stage copies the whole monorepo root and installs via npm
5+
# workspaces rather than `npm ci` inside this directory alone.
6+
FROM node:22-slim AS build
7+
WORKDIR /app
8+
COPY package.json package-lock.json ./
9+
COPY packages/loopover-engine/package.json packages/loopover-engine/package.json
10+
COPY packages/discovery-index/package.json packages/discovery-index/package.json
11+
RUN npm ci --workspace @loopover/engine --workspace @loopover/discovery-index --include-workspace-root
12+
COPY packages/loopover-engine packages/loopover-engine
13+
COPY packages/discovery-index packages/discovery-index
14+
RUN npm run build --workspace @loopover/engine \
15+
&& npm run build --workspace @loopover/discovery-index \
16+
&& npm prune --omit=dev --workspace @loopover/discovery-index
17+
18+
FROM node:22-slim AS runtime
19+
# Least privilege: run as a non-root user.
20+
RUN useradd --create-home --uid 10001 discovery-index
21+
WORKDIR /app
22+
ENV NODE_ENV=production
23+
COPY --from=build /app/node_modules ./node_modules
24+
COPY --from=build /app/packages/loopover-engine/node_modules ./packages/loopover-engine/node_modules
25+
COPY --from=build --chown=discovery-index:discovery-index /app/packages/loopover-engine/dist ./packages/loopover-engine/dist
26+
COPY --from=build --chown=discovery-index:discovery-index /app/packages/loopover-engine/package.json ./packages/loopover-engine/package.json
27+
COPY --from=build --chown=discovery-index:discovery-index /app/packages/discovery-index/dist ./packages/discovery-index/dist
28+
COPY --from=build --chown=discovery-index:discovery-index /app/packages/discovery-index/package.json ./packages/discovery-index/package.json
29+
WORKDIR /app/packages/discovery-index
30+
USER discovery-index
31+
ENV PORT=8080
32+
EXPOSE 8080
33+
# Provide at runtime (NOT baked into the image): DISCOVERY_INDEX_SHARED_SECRET (bearer auth for callers),
34+
# DISCOVERY_INDEX_GITHUB_TOKEN (this service's own GitHub token, isolated from any other component's).
35+
CMD ["node", "dist/server.js"]

packages/discovery-index/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Discovery-index service
2+
3+
A standalone microservice implementing the hosted half of the [discovery plane](../loopover-miner/docs/discovery-plane-operator-guide.md) (#4250): a shared, cached GitHub issue/search index that opted-in `loopover-miner` instances can query instead of each independently fanning out to GitHub's search/listing APIs. Metadata-only — see [`@loopover/engine`'s discovery-index contract](../loopover-engine/src/discovery-index-contract.ts) for the exact public-safe candidate shape and the forbidden-field boundary this service can never cross.
4+
5+
This is optional, shared infrastructure to reduce duplicate GitHub API pressure across the miner fleet (the rate-limit incident this mitigates: #1936). Self-hosted AMS/ORB deployments are completely unaffected whether or not this service exists — opting in is a separate, default-off client change (#7168).
6+
7+
## API
8+
9+
| Route | Purpose |
10+
| --------------------------------- | ----------------------------------------------------------------------------------------------- |
11+
| `GET /health` | Liveness health check. |
12+
| `GET /ready` | Readiness — checks this service's own GitHub token is configured. |
13+
| `GET /metrics` | Prometheus text-format metrics. |
14+
| `POST /v1/discovery-index/query` | `Authorization: Bearer <DISCOVERY_INDEX_SHARED_SECRET>``DiscoveryIndexRequest``DiscoveryIndexResponse`. |
15+
16+
See `packages/loopover-engine/src/discovery-index-contract.ts` for the full request/response contract (`normalizeDiscoveryIndexRequest`/`normalizeDiscoveryIndexResponse`), which this service both consumes and emits through.
17+
18+
## Configuration
19+
20+
| Env var | Purpose |
21+
| ------------------------------------ | ----------------------------------------------------------------------------------------------- |
22+
| `DISCOVERY_INDEX_SHARED_SECRET` | Bearer secret required to call `/v1/discovery-index/*`. Unset ⇒ the service fails closed (503). |
23+
| `DISCOVERY_INDEX_GITHUB_TOKEN` | This service's own GitHub token, isolated from any other component's (REES, the main engine's installation tokens, etc.). Unset ⇒ `/ready` reports not-ready. |
24+
| `DISCOVERY_INDEX_CACHE_TTL_MS` | TTL for cached query results, per unique `(repos, orgs, searchTerms)` scope. Default `300000` (5 minutes). |
25+
| `PORT` | HTTP port. Default `8080`. |
26+
27+
## Deployment
28+
29+
Build and run via the included `Dockerfile` (build context = the **repository root**, since this service depends on the `@loopover/engine` workspace package):
30+
31+
```sh
32+
docker build -f packages/discovery-index/Dockerfile -t loopover-discovery-index .
33+
docker run -p 8080:8080 -e DISCOVERY_INDEX_SHARED_SECRET=... -e DISCOVERY_INDEX_GITHUB_TOKEN=... loopover-discovery-index
34+
```
35+
36+
Hosting/DNS/TLS/observability wiring for a public deployment is tracked separately (#7167), not part of this service's own code.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@loopover/discovery-index",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "LoopOver hosted discovery-index service (#4250) — a shared, cached GitHub issue/search fan-out and soft-claim coordinator opted-in miner instances can query instead of each independently hammering GitHub. Deploys standalone; see #7164.",
7+
"engines": {
8+
"node": ">=22.0.0"
9+
},
10+
"scripts": {
11+
"build": "tsc -p tsconfig.json",
12+
"start": "node dist/server.js",
13+
"dev": "node --experimental-strip-types --watch src/server.ts"
14+
},
15+
"dependencies": {
16+
"@hono/node-server": "^1.19.14",
17+
"@loopover/engine": "^3.4.0",
18+
"hono": "^4.12.27"
19+
},
20+
"devDependencies": {
21+
"@types/node": "^22.20.0",
22+
"typescript": "^5.9.3"
23+
}
24+
}

0 commit comments

Comments
 (0)