Review code changes on a feature branch compared to a base branch (default: master). Checks for pattern consistency, code duplication, unnecessary variables, and proper use of existing utilities...
Review code changes on a feature branch, checking for:
as any casts)exc=True on model loads (backend)[] vs null) and failures indistinguishable from empty results/branch-review [base-branch]
Arguments:
base-branch (optional): The branch to compare against. Defaults to master.git diff [base-branch]...HEAD --stat
git diff [base-branch]...HEAD
git log [base-branch]..HEAD --oneline
If the log shows commits addressing earlier review rounds ("Address review", "Address Codex round N"), treat those fixes as the riskiest part of the diff β see Cross-Cutting check 3.
For each significantly changed file, read the full file to understand context:
Read tool to examine new/modified filesCheck references/feature-documentation-index.md to find relevant architecture docs for the feature area being changed. Read those docs before reviewing to understand expected patterns.
Compare new code against existing patterns:
vuex-module-decorators patternsexc=True usageRestException imports, no HTTP concernsCodebase-specific review guidelines are in CLAUDE.md - read them before reviewing.
Produce a single, numbered list of findings β do not split issues into separate "major" and "minor" sections. Use a Severity column instead (High / Medium / Low / Nit). For each finding, include:
file:line)Then produce two numbered tables (see Example Output Format below):
Routing rules β do not invent new top-level sections:
This keeps every actionable item discoverable from the Findings Summary table by number, and the Checklist Coverage table answers "what was checked vs. what was flagged" without overlapping content.
| Category | Description |
|---|---|
| Pattern Consistency | Code that doesn't follow established patterns |
| Code Duplication | Logic that should be extracted to shared utilities |
| Unnecessary Variables | Temporary variables used only once |
| Missing Abstractions | Opportunities to use existing utilities |
| Performance Issues | N+1 queries, looped API/DB calls, missing batch endpoints |
| Type Safety | as any casts, missing types, unsafe assertions |
| Error Handling | Inconsistent or duplicate error handling |
| Layer Violation | API concerns in models or model concerns in API |
| Security / Access Control | Missing permission checks, bypassed access control |
| Raw PyMongo | Using Model().collection.find() instead of Model().find() |
| Redundant Validation | Checks that duplicate framework behavior |
| API calls in Vue components | Direct this.girderRest.get(...) / this.girderRest.post(...) in components instead of an API file (GirderAPI.ts, AnnotationsAPI.ts, etc.) |
| Frontend compensating for backend | Frontend fallback logic that masks backend issues or duplicates backend access control |
| Store Organization | New state added to src/store/index.ts instead of a focused store module |
| Naming | Generic variable names or function names that no longer match their behavior |
| Partial Persistence | One logical change issuing several writes of the same key, so a mid-sequence failure leaves the resource partially updated |
| Error Message Mangling | Store action throwing or propagating without rawError: true, so callers get ERR_ACTION_ACCESS_UNDEFINED instead of the reason |
| Symmetric Path Drift | One of two twin paths changed: create-styling β update-restyling, draw β retain/clear, register β teardown, one guarded call site β its siblings, client β server end of a contract |
| Empty-State Contract | Two of the three list states merged, in either direction (if ids: in Python; ids?.length guards or ?? [] defaults in TS), or a failure return indistinguishable from an empty success |
| Lossy Change Identity | Signature/hash that misses changes it exists to detect: sampled elements, undelimited nested lists, content changes with stable membership |
| Stale Derived State | Derived ids/caches surviving an input replacement or upstream change; sequence-guard token claimed after early returns |
| Stale String Reference | dispatch("...")/event-name string not updated when the member was renamed |
| Hidden-Mounted Work | Expensive work in hidden-but-mounted content not gated on visibility: mount-time work in FloatingPalette slots, per-scrub computeds in palettes or once-opened VWindowItem tabs |
| Overlay Stacking | New floating palette/panel absent from the shared right-edge clearance computation |
These apply to the whole diff, frontend and backend alike. All three shapes recurred across the multi-round reviews of PRs #1279, #1288 and #1298.
The most repeated finding shape in this repo: a rule applied to one of two symmetric paths. For every behavior the diff adds or changes, name its twin and check the twin β the two implementations rarely share a name, so search by concept:
onBeforeUnmount)An optional id-list parameter has three meanings: absent/null = "no constraint", non-empty = "these", present-but-empty = "none". Merging any two of those states silently rescopes the operation, in the direction of the merge: empty β absent turns "act on zero matches" into "act on everything"; absent β empty turns "act on everything" into "act on nothing" when the receiver honors the contract. In PR #1298 the two composed: exporting an empty filtered set silently downloaded the entire dataset β the client's annotationIds || [] merged "no constraint" (null) into [] before the request, and the server's if annotationIds: read [] back as "no constraint". Check:
if ids: treats [] as absent. TypeScript: arrays are always truthy, so if (ids) is safe β the merges are if (ids?.length) / ids.length > 0 ? ids : undefined (empty β absent) and || [] / ?? [] defaults (absent β empty, erasing "no constraint" before it reaches the wire).{}/[]/null-as-data makes a transient network error indistinguishable from a real empty result. In #1298 this became "every gate resolves to zero matches and the whole dataset disappears"; in #1279 a failed batch POST was misreported as successful deduplication. Failures must propagate (return null or throw), and the caller must leave existing derived state alone on failure.When the branch contains commits addressing earlier review rounds, review those fixes as first-class changes, not as settled ground. Across four rounds on PR #1298, most later findings were consequences of earlier fixes rather than of the original feature. For each fix ask:
fetchMatchingIds still posting the rejected payload. A diff adding the same guard at two call sites is a finding even when no bug is visible yet.When reviewing changes to devops/girder/plugins/AnnotationPlugin/, apply these additional checks:
Search for patterns like for ... in ...: Model().load( or [Model().load(id) for id in ids]. These should use $in queries instead.
server/models/) must NOT import or raise RestException. They should raise ValueError or ValidationException.server/api/) should handle all input parsing/conversion at the top of the method, then pass clean data to models.Flag any use of Model().collection.find() β should be Model().find(). The only exception is collection.aggregate() for aggregation pipelines.
Model().load(id, ...) followed by if result is None: raise .... Should use exc=True parameter instead.Model().load(id, force=True) unless there's a clear comment explaining why access checks are bypassed.Flag except Exception: or bare except:. These swallow errors like KeyboardInterrupt, MemoryError, etc. Catch specific exception types.
Even where a broad catch is justified (best-effort cleanup that must not mask the original error), the exception message must be surfaced, not just the fact that something failed (pchoisel, PR #1225):
logger.exception(...) β it appends the message and traceback; a logger.error("cleanup failed") with no exception info, or a bare pass, hides the one thing a debugger needs. (logger.exception outside an except block logs NoneType: None β use it only inside the handler.)ValueError β RestException, cleanup failure β RuntimeError), include str(e) in the message or chain with raise ... from exc.except X: pass) must catch the narrow exception the skip is designed for (e.g. girder_client.HttpError for an inaccessible resource), with a comment; except Exception: pass silently eats real bugs.WRITE or ADMIN access on the affected resource.ObjectId() conversion (the conversion itself raises on invalid input).Model().load(..., exc=True) (exc=True already raises).id, item, data when a more specific name is possible.For every new or modified @access.public endpoint:
.get() / len() / int() / indexing on request data must be validated at the API boundary via the shared helpers in server/helpers/validation.py (requireObjectBody, requireList, requireObjectId, requireInt, validateListInputs, ...), which raise RestException(code=400, ...). Flag new/edited endpoints that hand-roll inline isinstance guards instead of calling these. A malformed payload must produce a clean 400, never an uncaught 500. (Applies to @access.user endpoints too, not only @access.public.)except (bson.errors.InvalidId, TypeError) at the API boundary (InvalidId for a bad hex string, TypeError for a non-string like {"datasetId": 123}; InvalidId is NOT a ValueError). requireObjectId already does this.MAX_* constant β unauthenticated callers must not be able to force unbounded DB or serialization work.@memoizeBodyJson is justified ONLY when the endpoint is also @recordable and its findDatasetIdFn reads memoizedBodyJson. On any other endpoint it is noise: use a plain def handler(self, params) signature and call self.getBodyJson() directly (pattern: datasetView.py::create). Flag *args, **kwargs endpoint signatures that exist only to receive the memoized kwarg.getattr(self, "_cache", None) properties). Girder's Model() constructor already returns a cached singleton (_ModelSingleton metaclass) β construct it in __init__ like the existing self._annotationModel = AnnotationModel() idiom.MAX_*) belong at the top of the class definition, not between methods mid-file.$count output fields should be named count, not a cryptic short name β easier to debug. Dense $addFields/$cond/$ifNull stages need a comment explaining what the stage computes and why.The backend's human reviewer flags these reliably; catch them first.
Girder provides it β don't hand-roll it. These get flagged with a link to the Girder source:
self.getCurrentUser() returns the already-loaded user document β don't re-load it by id afterward.getServerMode() answers "is this production?" β don't invent env flags or tox-level configuration for it.Model() construction is a cached singleton (see check 11) β no lazy-loading properties.girder_client provides it too β check before raw gc.get/gc.post. The same rule applies client-side in the nimbusimage/ package (pchoisel, PR #1225): a hand-rolled gc.get("folder", parameters={...}) + client-side scan was a one-call gc.listFolder(parentId, parentFolderType="user", name=...), and a raw gc.post("folder", ...) with manually json.dumps-ed metadata was gc.createFolder(..., metadata=dict) (it JSON-encodes metadata itself). Before writing a raw REST call, check girder_client.GirderClient for a method covering the operation (listFolder, listItem, createFolder, createItem, uploadFileToFolder, listResource, addMetadataToFolder, ...) β server-side filtering via their parameters beats fetching everything and scanning.
Python stdlib idioms for float checks: math.isnan(x) / math.isinf(x), never the x != x NaN trick or equality against float("inf") (pchoisel, PR #1225).
Factorization habits:
Python idioms for readability:
collections.defaultdict(list), not conditional-insert dances.mylist.copy() / mydict.copy(), not list(x) / dict(x).Signature hygiene (extends check 11):
self on a function that isn't a method β move it to module level.No in-band sentinels: a reserved value inside a user-data field (a magic tag name in a values list that switches behavior) collides with legitimate user data β the mode belongs in a separate request argument. The sibling-field shape is the correct resolution, not a violation: the list-filter tags field {values: string[], exclusive: boolean} was questioned in review and ruled by-design, because exclusive is already a separate argument and a tag literally named "exclusive" works fine (codebaseDocumentation/PR1203-PAUL-REVIEW.md). Flag only values that live inside the data itself and change behavior.
Answer the reviewer's questions before review does. For each new parameter or data path, the questions asked in past rounds: can this be None? What happens on re-run/re-import when the data already exists (overwrite, duplicate, or delete-old)? What happens to non-scalar values (a dict landing in a CSV cell)? Handle it in code, or say why not in a comment.
Why-comments on non-obvious mechanisms ("for future us"): anything relying on subtle semantics β aggregation stages, context-manager/GC behavior β needs a comment saying why it matters, written for a reader who doesn't know that corner of Python or Mongo.
For every new function or method in the diff, ask where it belongs before reviewing what it does (pchoisel, PR #1225 flagged a generic boolean-body parser defined as a staticmethod in an API resource class):
server/api/*.py belong in server/helpers/validation.py, next to requireInt/requireObjectId/optionalBoolean β where the other API files can find and reuse them. Endpoint-specific schema validators (ones encoding this endpoint's field names, dimensions, or messages) may stay in the API file.server/helpers/ or the model layer, not copy-pasted or parked in whichever API file needed it first.self or the class is a hint it is a utility that belongs at module level or in a helper module.src/store/*API.ts, shared utilities in src/utils/.
The test: "if a second endpoint needed this tomorrow, would it import from here?" If importing from an API module feels wrong, the function is in the wrong place today.When reviewing changes to src/, apply these additional checks:
Flag any direct this.girderRest.get(...) or this.girderRest.post(...) calls in Vue components. These should be methods in GirderAPI.ts, AnnotationsAPI.ts, or the appropriate API file.
Flag Promise.all(items.map(item => api.updateItem(item))) patterns. Suggest using or creating a batch endpoint instead.
Flag fallback patterns like "try new API, catch error, try old API". The frontend should trust the backend API. Double implementations create maintenance debt.
New state for distinct feature areas should go in a new store module, not src/store/index.ts (already 2000+ lines).
Diff-scan for defaults that changed as a side effect: items-per-page values, initial slider/config values, prop defaults, sort orders. The user has caught these post-merge more than once (e.g. a list default silently going 10 β 50). If a default changed and the change isn't the point of the branch, flag it.
Flag diffs in generated or documentation trees the branch shouldn't touch (e.g. codebaseDocumentation/api_documentation/ regenerated by a docs tool). Ask whether they're intentional rather than assuming.
For list/selection UIs: can a selected set survive a filter or mode change and then feed a bulk destructive action (delete/tag/hydrate)? Check that selection is scoped or cleared when the visible set's definition changes, and that select-all paths respect lazy-mode/budget caps instead of operating on the entire dataset.
syncConfiguration(key) PUTs the whole key, so N single-field calls in one operation = N writes of the same key, and a failure part-way leaves the shared collection partially updated while the operation reports failure. Distinct from the looped-call check (#2): the repeats here are sequential awaits on different fields, not a .map(), so they don't look like a loop.
For any handler that changes several fields of one resource, check:
changeLayer and saveContrastInConfiguration both write layers.)rawError: truevuex-module-decorators replaces any error escaping a bare @Action with a generic ERR_ACTION_ACCESS_UNDEFINED message. Flag a new/edited action if it throws or merely propagates (awaits an API call or another action) and any caller reads error.message. Errors are re-wrapped at every boundary they cross, across modules, so check the whole chain, not just the action in the diff. tsc/lint/tests stay green either way, and a test that mocks the action bypasses the decorator entirely β so a real-dispatch test asserting the exact message is the only regression guard (substring assertions pass regardless; the wrapper embeds the original message in its stack). See nimbus-frontend skill.
Any signature/hash whose comparison decides "skip the refetch / skip the recompute / skip clearing the selection" must change for every change it exists to detect (src/utils/signatures.ts). Flag:
cyrb53, memoized by array identity because these arrays are replaced wholesale).[["a"], ["b"]] and [["a","b"], []] must hash differently β feed a boundary marker or the row's id per row.JSON.stringify it β id-membership filters hold tens of thousands of ids and those getters rebuild on every frame scrub.For any action shaped "read inputs β maybe bail out β fetch β commit derived state":
filters.ts on one branch; correct precedent: properties.ts::ensureVisiblePropertyValues.dispatch("name"), commit("name") and event names are invisible to tsc: after a rename, the stale string logs an unknown-action error and resolves as a silent no-op, usually masked by a watcher doing similar work. When the diff renames a store member, sweep every string reference against the declared members. PR #1298 adds src/__tests__/dispatchedActions.test.ts to police dispatch names β check that a rename updates it rather than gets exempted from it.
Two container lifecycles hide content without unmounting it, with different cost profiles β don't conflate them:
FloatingPalette mounts its slot content immediately and hides it with display: none, so onMounted runs at every dataset load and computeds re-evaluate on every store change even if the panel has never been opened (#1298: an invisible WebGL Plotly render at dataset startup; #1288: a full connection scan not gated on timelapse mode).VWindowItem tab mounts lazily on first activation, then stays mounted and is hidden with v-show (documented in AnnotationBrowser.vue), so its cost starts at first open but persists from the hidden tab afterward (#1279: scoped counts scanning every annotation per scrub once the Connections tab had been opened).The shape recurred in three consecutive feature PRs. Flag:
:visible prop.A new floating palette or edge-anchored panel must join the shared right-edge clearance/stacking computation, and the check is combinatorial: consider every palette that can be open at the same time, not just the pair the feature was tested with. Recurred in #1288 (clearance computed from two of five right-edge palettes; action panels unhittable at 1280 px beneath a z-index-1006 palette) and #1298 (the Analysis panel covered the Filters panel β the very panel its own over-cap message told the user to open). Check both geometry (offsets, max-height) and z-index tiers (FloatingPalette 1006 vs action panels 1000).
The output has exactly four sections, in this order: Overall Assessment, Findings, Findings Summary, Checklist Coverage. Questions for Clarification is optional and appears only if there are open questions. There is no separate "Minor Observations" section β small items become findings with Severity Nit.
The two tables are numbered and serve different purposes:
## Code Review: [branch-name]
### Overall Assessment
[1β3 sentences: scope of the diff, overall quality, and anything notable that is NOT a finding β e.g. positive confirmations such as "backend access control is intact" or "no looped DB calls introduced". Do not list issues here.]
### Findings
#### Finding 1: [Short title]
- **File:** `src/store/example.ts:42`
- **Severity:** High | Medium | Low | Nit
- **Category:** [one of the Issue Categories rows, e.g. Pattern Consistency]
**Current:**
\`\`\`typescript
// problematic code
\`\`\`
**Suggested:**
\`\`\`typescript
// improved code
\`\`\`
**Rationale:** [Why this change improves the code]
---
#### Finding 2: [Short title]
- **File:** `β¦`
- **Severity:** β¦
- **Category:** β¦
**Current:** β¦ **Suggested:** β¦ **Rationale:** β¦
---
### Findings Summary
| # | Severity | Category | Location | Summary |
|---|----------|----------|----------|---------|
| 1 | Low | Pattern Consistency | `src/store/example.ts:42` | one-line restatement of Finding 1 |
| 2 | Nit | Code Duplication | `src/components/Foo.vue:17` | one-line restatement of Finding 2 |
### Checklist Coverage
| Category | Status | Findings |
|----------|--------|----------|
| Pattern Consistency | warn | #1 |
| Code Duplication | warn | #2 |
| Unnecessary Variables | pass | β |
| Missing Abstractions | pass | β |
| Performance Issues (looped DB/API) | pass | β |
| Type Safety | pass | β |
| Error Handling | pass | β |
| Layer Violation (API vs model) | n/a | β |
| Security / Access Control | pass | β |
| Raw PyMongo | n/a | β |
| Redundant Validation | pass | β |
| API calls in Vue components | pass | β |
| Frontend compensating for backend | pass | β |
| Store Organization | pass | β |
| Naming | pass | β |
| Partial persistence (same key written twice) | pass | β |
| Actions that throw/propagate without rawError | pass | β |
| Symmetric path drift | pass | β |
| Empty-state contract ([] vs null, failure vs empty) | pass | β |
| Lossy change identity | pass | β |
| Stale derived state | pass | β |
| Stale string reference | pass | β |
| Hidden-mounted work | pass | β |
| Overlay stacking | pass | β |
### Questions for Clarification
[Only include this section if there are open questions. Otherwise omit it.]
- [Question that needs the author's input]
Notes on the tables:
n/a for categories that don't apply to the diff (e.g. backend-only checks on a frontend-only PR).CLAUDE.mdreferences/feature-documentation-index.md