Context
Build the standalone review-enrichment/ Node/TS microservice (REES) that the engine seam calls. THIS issue ships the scaffold only: an HTTP server with health/ready + a /v1/enrich endpoint that authenticates, reads per-repo config, runs the orchestrator skeleton (no analyzers yet — returns an empty/partial:false brief), and deploys to Railway. Per-analyzer logic lands in separate issues behind this seam. Mirrors the engine's existing root Dockerfile/railway.json deploy pattern and the symmetric shared-secret bearer auth of src/auth/security.ts.
Requirements
- Own Node/TS app under
review-enrichment/ (NOT bundled into the engine or Worker). Runs NO LLM — purely deterministic + external-API analysis — so it cannot leak the AI subscription token and adds zero AI spend.
- Symmetric shared-secret bearer auth:
REES_SHARED_SECRET (operator-minted, e.g. createOpaqueToken('rees')) on BOTH engine and service env. The service verifies with a constant-time compare (node:crypto over sha256 bytes, byte-for-byte the pattern at src/auth/security.ts:84). Mismatch → 401 no body. Bind only on Railway's private network; /health//ready are unauthenticated liveness only.
- Per-repo config read from
GITTENSORY_REPO_CONFIG_DIR=/config/{owner}__{repo}/.gittensory.yml with a /config/.gittensory.yml global fallback — the SAME folder/global-fallback layout the branch already introduced for the other features:; the service reads the optional enrichment: block (analyzers list, license allow/deny, per-analyzer caps, timeoutMs).
- The service NEVER throws into the engine: it always returns its best partial brief;
200 with partial:true when an analyzer is missing/degraded. Non-200 only for 401 (bad auth), 400 (malformed), 503 (overloaded).
- HTTP contract:
POST /v1/enrich with Authorization: Bearer, Idempotency-Key: <repo>#<pr>#<headSha>; request/response envelopes per the design (request carries diff+files so NO repo checkout is needed; response carries schemaVersion, analyzerStatus, partial, findings, promptSection, systemSuffix). The optional short-lived githubToken is held in-memory for the request only, NEVER logged or cached.
Deliverables
review-enrichment/package.json, src/server.ts (hono: /health, /ready, POST /v1/enrich), src/auth.ts (timingSafeEqual bearer mirror), src/config.ts (read GITTENSORY_REPO_CONFIG_DIR/{owner}__{repo}/.gittensory.yml + global fallback), src/brief.ts (orchestrator skeleton: Promise.allSettled over an empty analyzer set within budget.timeoutMs, assembles ReviewBrief JSON + analyzerStatus/partial), src/render.ts (ReviewBrief → public-safe promptSection, with the secret/value redaction discipline so a finding cites kind+line, never the value). Add a Redis per-headSha idempotency cache hook (the stack already runs Redis) keyed repo#pr#headSha.
review-enrichment/Dockerfile: multi-stage node:24-slim non-root, npm ci, bundle src/server.ts → dist/server.mjs, EXPOSE 8080, HEALTHCHECK probing /ready, CMD ["node","dist/server.mjs"]. (Analyzer toolchain is baked in the per-analyzer issues that need it.)
review-enrichment/railway.json: DOCKERFILE builder with watchPatterns: ["review-enrichment/**"] (so engine-only pushes don't rebuild the service and vice-versa), healthcheckPath: "/ready", restartPolicyType: "ON_FAILURE", numReplicas: 1.
- Deploy: as ghost@aethereal.dev create a NEW service in the existing
gittensory-selfhost-test Railway project; set REES_SHARED_SECRET + REES_URL on both engine and service; push so Railway auto-deploys.
Acceptance criteria
/ready returns 200 in the deployed Railway service; /v1/enrich with a valid bearer returns a well-formed (empty-findings, partial:false) brief; a bad/missing bearer returns 401 with no body via constant-time compare.
- The service depends on NO repo checkout (operates on the request diff+files) and never logs the
githubToken or any secret value.
- Unit tests for the auth bearer (match/mismatch, constant-time path), the config loader (per-repo + global fallback + missing-file), and the render redaction (a planted secret is never emitted verbatim). Service code is not measured by Codecov (only
src/** is), but include the tests so the scaffold is regression-guarded.
npm audit --audit-level=moderate clean for the new package. (No src/** change in this issue, so no Codecov patch impact.)
Parent: #1029
Context
Build the standalone
review-enrichment/Node/TS microservice (REES) that the engine seam calls. THIS issue ships the scaffold only: an HTTP server with health/ready + a/v1/enrichendpoint that authenticates, reads per-repo config, runs the orchestrator skeleton (no analyzers yet — returns an empty/partial:falsebrief), and deploys to Railway. Per-analyzer logic lands in separate issues behind this seam. Mirrors the engine's existing rootDockerfile/railway.jsondeploy pattern and the symmetric shared-secret bearer auth ofsrc/auth/security.ts.Requirements
review-enrichment/(NOT bundled into the engine or Worker). Runs NO LLM — purely deterministic + external-API analysis — so it cannot leak the AI subscription token and adds zero AI spend.REES_SHARED_SECRET(operator-minted, e.g.createOpaqueToken('rees')) on BOTH engine and service env. The service verifies with a constant-time compare (node:cryptoover sha256 bytes, byte-for-byte the pattern atsrc/auth/security.ts:84). Mismatch →401no body. Bind only on Railway's private network;/health//readyare unauthenticated liveness only.GITTENSORY_REPO_CONFIG_DIR=/config/{owner}__{repo}/.gittensory.ymlwith a/config/.gittensory.ymlglobal fallback — the SAME folder/global-fallback layout the branch already introduced for the otherfeatures:; the service reads the optionalenrichment:block (analyzers list, license allow/deny, per-analyzer caps, timeoutMs).200withpartial:truewhen an analyzer is missing/degraded. Non-200 only for401(bad auth),400(malformed),503(overloaded).POST /v1/enrichwithAuthorization: Bearer,Idempotency-Key: <repo>#<pr>#<headSha>; request/response envelopes per the design (request carries diff+files so NO repo checkout is needed; response carriesschemaVersion, analyzerStatus, partial, findings, promptSection, systemSuffix). The optional short-livedgithubTokenis held in-memory for the request only, NEVER logged or cached.Deliverables
review-enrichment/package.json,src/server.ts(hono:/health,/ready,POST /v1/enrich),src/auth.ts(timingSafeEqual bearer mirror),src/config.ts(readGITTENSORY_REPO_CONFIG_DIR/{owner}__{repo}/.gittensory.yml+ global fallback),src/brief.ts(orchestrator skeleton:Promise.allSettledover an empty analyzer set withinbudget.timeoutMs, assemblesReviewBriefJSON +analyzerStatus/partial),src/render.ts(ReviewBrief → public-safepromptSection, with the secret/value redaction discipline so a finding cites kind+line, never the value). Add a Redis per-headSha idempotency cache hook (the stack already runs Redis) keyedrepo#pr#headSha.review-enrichment/Dockerfile: multi-stagenode:24-slimnon-root,npm ci, bundlesrc/server.ts→dist/server.mjs,EXPOSE 8080,HEALTHCHECKprobing/ready,CMD ["node","dist/server.mjs"]. (Analyzer toolchain is baked in the per-analyzer issues that need it.)review-enrichment/railway.json:DOCKERFILEbuilder withwatchPatterns: ["review-enrichment/**"](so engine-only pushes don't rebuild the service and vice-versa),healthcheckPath: "/ready",restartPolicyType: "ON_FAILURE",numReplicas: 1.gittensory-selfhost-testRailway project; setREES_SHARED_SECRET+REES_URLon both engine and service; push so Railway auto-deploys.Acceptance criteria
/readyreturns 200 in the deployed Railway service;/v1/enrichwith a valid bearer returns a well-formed (empty-findings,partial:false) brief; a bad/missing bearer returns401with no body via constant-time compare.githubTokenor any secret value.src/**is), but include the tests so the scaffold is regression-guarded.npm audit --audit-level=moderateclean for the new package. (Nosrc/**change in this issue, so no Codecov patch impact.)Parent: #1029