ReadonlyREST PR Review
The goal is the level of insight a careful senior Scala/Elasticsearch reviewer would produce — not a rubber-stamp "No issues found."
Mandatory methodology
- Read CLAUDE.md first. Then read the PR description and any prior review comments — they often contain context, previous findings, and the contributor's responses you must not duplicate or contradict without reason.
- Pull the full diff and full context. Use
gh pr diff <num>for the diff;gh pr view <num> --json title,body,commits,reviewsfor metadata;gh api repos/.../issues/<num>/commentsfor prior discussion. Do not rely on a truncated diff — if the diff is large, process it in chunks but cover every file. - Read the modified files in full (not just the hunks). Bugs usually live in the lines around the diff, not inside it. Use
Readon each changed file before commenting on it. - Cross-reference call sites. When a function/type is touched,
grep -rnfor every caller and assess whether the change is safe for each. A behavioral change that compiles is not necessarily a behavioral change that's correct. The Scala compiler's variance and implicit-resolution machinery hides a lot of subtle behavior — always verify the runtime semantics. - Check the tests. For every behavioral change, ask: does a test cover this? If a test claims to cover it, does the test actually verify the new behavior, or does it pass for an incidental reason (false-confidence test)? Property-based tests with overly-narrow generators are a common variant.
- Check CI. Use
gh pr checks <num>andgh run view <run-id> --log-failedfor any failing job. A failing integration test on a single ES module is often a hint that the code change broke a real flow on that ES version. - Empirically verify claims. If the diff includes a regex, parser, encoder, or security check — run it.
node -e "/regex/.test('input')"is fair game for regexes;scala-cliif available for Scala snippets. Do not approve a security-sensitive regex without trying bypasses.
What to actively hunt for
Hunt for these categories on every PR, not just security-flagged ones:
- Cross-ES-version drift. This is the #1 source of bugs in this repo. The plugin supports ES 6.7 → 9.2 via 30+
es{version}x/adapter modules. When a change touches one module, check whether the same fix needs to apply to siblings. TheMultiGetEsRequestContext/MultiSearchEsRequestContextfiles in particular often need identical changes across all es modules. Usegit diff --statto check; if the diff touches onlyes814xbut the changed code exists verbatim ines815x, flag it. - Internal Scala runtime APIs. Any usage of
scala.runtime.ScalaRunTime._*,_-prefixed runtime methods, or other implementation-detail APIs is forbidden — they change silently between Scala versions. Public alternatives exist (scala.util.hashing.MurmurHash3.productHashfor hashCode, etc.). Recent real example: PR #1247 usedScalaRunTime._hashCode(this)for cached case-class hashCode; fixed by extracting anEagerHashCodetrait usingMurmurHash3.productHash. - Hot-path allocation patterns. This codebase processes ACL evaluation per-request. Watch for:
foldLeftover an immutableSet(allocates O(n) intermediate Sets — usepartitionormutable.Set.Builderinstead); repeated.filter(...).map(...)chains where a single pass would do; computinghashCodelazily on objects appearing in high-frequencySet/Mapoperations. - Dead-code branches. A new branch that can never fire because an earlier branch always matches first. Common pattern: a fallback strategy preempted by an unconditional default in the same caller.
- False-confidence tests. Tests that appear to assert the new behavior but actually pass via an incidental code path. Stress-test by mentally running the test with the fix reverted — does the test still pass? If yes, the test is decorative. Property-based tests with overly-narrow generators are a common offender.
- Behavioral changes without test coverage. Look for code that changes the shape of a response, the conditions under which a side-effect fires, or the result of a function — without a corresponding test addition.
- Silent removals. A condition, branch, or feature flag deleted with no replacement. Search for the deleted identifier across the codebase to confirm intent.
- Wrong enum style. Sealed traits with implicit case-object children but no
enumeratum.Enum[T]mixin and nofindValues— they break JSON encoders/decoders that rely on enumeratum's introspection. Alwaysenumeratum. - Raw
Futureinstead ofmonix.eval.Task. All async work in this codebase usesTask. Adding aFuture-based code path forces an awkward conversion at the boundary and breaks structured concurrency. println/System.out/System.err. Forbidden — use structured logging.-Xfatal-warningsviolations. Compilation will fail anyway, but flag obvious cases: unused imports/params/locals/privates, deprecated API usage, exhaustiveness warnings onmatch.- Missing GNU GPL v3 license header. Every new
.scalafile must carry it. The pre-commit hook normally catches this, but PRs from external contributors may slip through. - Internal-Elasticsearch-API misuse. Anything in
org.elasticsearch.*outside the public APIs may break between minor versions. Particularly check usages added ines{version}x/modules. - Shadow/shading boundaries. Direct imports of un-shaded dependency packages (e.g.
com.google.gson,org.apache.logging) should go through the relocatedtech.beshu.ror.*prefix. The Shadow plugin should hide all third-party deps. - Unrelated changes bundled. A PR titled "fix LDAP timeout" should not also change FLS evaluation or settings parsing. Note bundled drift in the review.
- Domain-invariant violations. For changes touching
kibana_access, the fields/FLS rule, audit, or/_readonlyrestAPIs, check the design invariants in theror-internalsskill (decision-tree ordering, hiddenluceneFLS engine, 5KB audit-event cap, endpoint→action-name mapping).
Project-convention checklist (apply mechanically)
Futureinstead ofmonix.eval.Taskin new codeprintln/System.out/System.errinstead of structured logging- Sealed trait + case objects without
enumeratum.Enum[T]/EnumEntry scala.runtime.ScalaRunTime._*or other_-prefixed internal Scala APIs- Missing GNU GPL v3 license header on new
.scalafiles -Xfatal-warningsviolations (unused imports/params/locals/privates)- Identical change applied to one
es{version}xmodule but missed in siblings where the same code path exists - Direct imports of un-shaded dependency packages (should go through
tech.beshu.rorprefix) - Comment defects (
docs/dev/code-style.md): narrating the change ("no longer set here", "moved from X") instead of the current state; explaining a function at its call site instead of at its definition; restating self-describing code Future.successful(...)wrappers around already-synchronous values insideTaskflows (anti-pattern: just lift directly withTask.now/Task.pure)
Output format
BE CONCISE. The review is read by a busy maintainer. Every finding = what to do + why. Nothing else.
- Findings only, ordered by severity (
🚨 Critical→⚠️ Warning→Nit). Each finding, max 4 lines:file:line— imperative one-liner: what to change.- Why, in 1–2 sentences, with the single strongest piece of evidence (a grep hit, a failing input, a call site) — not the full investigation.
- A code snippet ONLY when the fix isn't obvious from the one-liner.
- NO "What I checked" narrative, NO "What looks good" section, NO methodology recap, NO restating the PR description. Do the investigation; don't serialize it.
- Open questions: only if the answer would change a finding; max 2.
- Genuinely no issues? One line:
No issues found — verified <the 3–5 highest-risk things you checked>.That line IS the required backing; never a bare "No issues found". - Write the review itself in the repo style: Simplified Technical English plus Zinsser (
docs/dev/writing-style.md). One idea per sentence, active voice, no metaphor. - Hard cap: ~30 lines / ~350 words total. With many findings, keep every Critical/Warning full and compress Nits to one line each.
When the PR is small / docs-only
Same rules, shorter: the no-issues one-liner (with what you verified) is a complete review for a docs-only diff you read in full.