Reviewing an Amber PR
You are the maintainer. The rubric is the law: REVIEW_RUBRIC.md is the single source of truth for the severity framework, per-type bars, and enhancement-vs-feature values; DESIGN_DECISIONS.md lists intentional trade-offs you must discount. This skill is only the procedure — it does not restate the rubric.
Golden rule: the PR description is a claim, not evidence. Verify everything against the diff and against running code. Cite file:line for every point you make.
1. Pull the PR and check mergeability mechanics
gh pr checkout <number>
gh pr view <number> --json title,author,body,files,additions,deletions
gh pr view <number> --json mergeable,mergeStateStatus,reviewDecision
gh pr diff <number>
Read the mechanics before you read the code (see rubric §6):
mergeable: CONFLICTING -> not reviewable yet; ask for a rebase.
mergeStateStatus: BLOCKED -> usually a required review/approval, not a merge conflict. Check reviewDecision; do not chase a phantom conflict.
reviewDecision: REVIEW_REQUIRED / CHANGES_REQUESTED -> exactly that.
- Skim the diff once end-to-end now. Note its true scope (does it match the title?) before forming opinions.
2. Compile and run specs locally
A PR with red specs or bin/ameba errors gets bounced, not reviewed (rubric §6, §7). Match CI before spending review attention:
shards install # produces bin/ameba
bin/amber_spec # ./bin/ameba, crystal tool format --check, crystal spec, crystal spec ./spec/build_spec_granite.cr
- The granite spec needs a local Postgres (DB
granite_test, user postgres, password postgres); start one or note it as not-run.
- CI pins Crystal 1.9.2 for specs. If it's green for you on a newer compiler, suspect a version-pin break and re-check against the declared range (
>= 1.0.0, < 2.0).
- Green specs do not mean correct. If the suite is happy-path only, green tells you nothing about the edge cases the change can break (§3 Performance). Hold that thought for step 5.
3. Classify the PR type
Pick exactly one type and review against that bar (rubric §3): Bugfix · Security · Performance · Refactor · Feature/default-change. State your choice in the review. If the author claimed a type, confirm the diff actually fits it — a "refactor" that edits spec assertions is not a refactor; a "perf" change that alters output is a behavior change.
4. Apply the severity framework — verify, then discount
For every issue you raise, assign a tier (Critical / High / Medium / Low / Informational per rubric §2), then discount it through the three levers. A raw finding is a hypothesis; the surviving severity is what you report. Verify each lever yourself in the diff/code — do not inherit the author's framing.
- (a) Reachability + existing mitigations. Trace the finding to a sink an attacker can actually drive, then check for
rescue on the immediate caller and a top-level Pipe::Error/framework handler before assigning severity. Worked example: the MessageVerifier/MessageEncryptor IndexError "DoS" was fully contained by SignedStore#verify, EncryptedStore#verify_and_decrypt, and Pipe::Error -> Informational/Low, not High. If a mitigation exists, name it and discount.
- (b) Threat model. Network-facing vs local dev CLI. A
bin/amber/CLI command already runs as the operator, so the encrypt.cr system("#{ed} #{file}") injection is Low (operator is the attacker). The same shape on a network endpoint jumps several tiers.
- (c) Environment gating. Where does the dangerous branch run? The
error.cr unescaped @ex.message is in the non-development branch, so it's a production reflected-XSS (High); a finding that only fires in development? is usually Low/Informational because production never executes it.
Do not accept a severity from the description. Re-derive it from the three levers, citing the lines.
5. Apply the per-type correctness bar
Grade the diff against the bar for the type you picked in step 3 (rubric §3). The bar is evidence, not vibes:
- Bugfix: a spec that was red before the fix and is green now; root-cause not symptom; blast-radius note.
- Security: clears all of §3 Security — real+reachable+not-already-mitigated; complete fix (the
encrypt.cr fix that escaped the file but left $EDITOR injectable failed this — prefer eliminating the shell via Process.run(cmd, [args])); narrow fix (no over-broad rescue, no loosened auth check); an adversarial spec that sends the malicious input and asserts neutralization.
- Performance: provably behavior-identical. Require the differential edge-case table (dotfiles, trailing slash, empty/extension-only segments) — this is exactly where the
File.extname router PR diverged while CI stayed green. No table, no merge.
- Refactor: existing suite passes unchanged; if assertions were edited, it's not a pure refactor — reclassify.
- Feature/default-change: tests for the new behavior and its edge cases; docs/changelog updated; description matches code; extra scrutiny per step 7.
6. Discount documented design trade-offs
Before you flag anything that smells like "why is it done this way," scan DESIGN_DECISIONS.md. If an entry matches, it is by design — do not flag it, and reject a PR that "fixes" it without a separate discussion. Common ones the wave tripped on:
- App defaults live in the CLI app template, not core — so "core has no default for X" is often correct, and a pipe not wired into
config/routes.cr.ecr is dead code.
crystal-db's pool is already concurrency-safe — don't accept a re-added Mutex around checkout.
error.cr is the non-development page (dev uses the exception_page shard) — review it for the production threat model, not its markup.
Process.run(cmd, [args]) (no shell) is the settled shell-out idiom; headers["X"] ||= value is the intentional non-clobbering default.
- Extension/content-type lists derive from
Content::TYPE / TYPE_EXT_REGEX — a hand-coded duplicate that can drift is flaggable.
A finding that contradicts an entry needs strong specific evidence (a concrete reachable failure or differential test), not "looks wrong."
7. Decide enhancement vs feature
Apply the rubric §4 values:
- Enhancement (improves something Amber already does without changing its surface contract) -> review against its matching §3 type bar plus evidence it doesn't change behavior for existing apps. Welcome as a PR.
- New feature (new pipe / CLI command / config knob / template default) -> Feature/default-change bar plus design buy-in; it should usually have started as an issue/discussion. A drive-by feature PR with no prior agreement is a reason to ask for that discussion first.
- Default/behavior changes get extra scrutiny. They change every app silently on upgrade — recall the SecureHeaders PR that shipped HSTS on-by-default (can hard-break HTTP) and was inert dead code. Confirm the semver call-out in the PR: a default/behavior change is not patch-safe (minor at minimum, major if it can break existing requests).
8. Apply AI-PR hygiene
AI-assisted PRs are welcome (rubric §5) — hold them to the same checklist:
9. Produce the verdict
End with one verdict and the evidence behind it. Every claim cites file:line.
- MERGE — fits its type bar, levers leave no live high/critical, no design-decision conflict, mechanics clean. Say what you verified.
- MERGE_WITH_CHANGES — sound and mergeable after small, named fixes (e.g. add the one missing adversarial spec, wire the pipe into
config/routes.cr.ecr, drop the machine-specific note). List each required change with its file:line.
- REQUEST_CHANGES — fails a bar or a lever in a way that needs author rework (incomplete security fix, missing differential table, over-broad
rescue, description/code mismatch). State the bar it missed and what evidence would clear it.
- CLOSE — wrong by design (contradicts a DESIGN_DECISIONS entry without evidence), a non-issue after discounting (the "DoS" that was fully mitigated), a duplicate of settled work, or a feature with no design buy-in that belongs in a discussion. Point to the rubric/decision that settles it.
Verdict template:
Type: <Bugfix|Security|Performance|Refactor|Feature/default-change>
Mergeability: mergeable=<...> mergeStateStatus=<...> reviewDecision=<...>
Local checks: bin/amber_spec <pass|fail|granite not-run>
Findings:
- [<Critical|High|Medium|Low|Informational>] <one line> (file:line) — levers applied: <a/b/c result>
Design-decision discounts: <entry or none>
Enhancement vs feature: <which, + semver note if default/behavior change>
AI hygiene: <pass | list gaps>
Verdict: <MERGE|MERGE_WITH_CHANGES|REQUEST_CHANGES|CLOSE>
Required to clear: <named changes with file:line, or n/a>
1---2name: review3description: Use when reviewing or triaging an Amber PR. Pulls the PR, checks mergeability mechanics, compiles and runs specs locally, classifies the PR type, applies the severity framework via the three discount levers (verifying claims rather than trusting the description), enforces the per-type correctness bar, discounts documented design trade-offs, decides enhancement vs feature, applies AI-PR hygiene, and produces a verdict (MERGE / MERGE_WITH_CHANGES / REQUEST_CHANGES / CLOSE) backed by file:line evidence.4---56# Reviewing an Amber PR78You are the maintainer. The rubric is the law: [REVIEW_RUBRIC.md](../../REVIEW_RUBRIC.md) is the single source of truth for the severity framework, per-type bars, and enhancement-vs-feature values; [DESIGN_DECISIONS.md](../../DESIGN_DECISIONS.md) lists intentional trade-offs you must discount. This skill is only the *procedure* — it does not restate the rubric.910Golden rule: **the PR description is a claim, not evidence.** Verify everything against the diff and against running code. Cite `file:line` for every point you make.1112## 1. Pull the PR and check mergeability mechanics1314```sh15gh pr checkout <number>16gh pr view <number> --json title,author,body,files,additions,deletions17gh pr view <number> --json mergeable,mergeStateStatus,reviewDecision18gh pr diff <number>19```2021Read the mechanics before you read the code (see rubric §6):2223- `mergeable: CONFLICTING` -> not reviewable yet; ask for a rebase.24- `mergeStateStatus: BLOCKED` -> usually a required review/approval, **not** a merge conflict. Check `reviewDecision`; do not chase a phantom conflict.25- `reviewDecision: REVIEW_REQUIRED` / `CHANGES_REQUESTED` -> exactly that.26- Skim the diff once end-to-end now. Note its true scope (does it match the title?) before forming opinions.2728## 2. Compile and run specs locally2930A PR with red specs or `bin/ameba` errors gets bounced, not reviewed (rubric §6, §7). Match CI before spending review attention:3132```sh33shards install # produces bin/ameba34bin/amber_spec # ./bin/ameba, crystal tool format --check, crystal spec, crystal spec ./spec/build_spec_granite.cr35```3637- The granite spec needs a local Postgres (DB `granite_test`, user `postgres`, password `postgres`); start one or note it as not-run.38- CI pins **Crystal 1.9.2** for specs. If it's green for you on a newer compiler, suspect a version-pin break and re-check against the declared range (`>= 1.0.0, < 2.0`).39- **Green specs do not mean correct.** If the suite is happy-path only, green tells you nothing about the edge cases the change can break (§3 Performance). Hold that thought for step 5.4041## 3. Classify the PR type4243Pick exactly one type and review against *that* bar (rubric §3): **Bugfix · Security · Performance · Refactor · Feature/default-change.** State your choice in the review. If the author claimed a type, confirm the diff actually fits it — a "refactor" that edits spec *assertions* is not a refactor; a "perf" change that alters output is a behavior change.4445## 4. Apply the severity framework — verify, then discount4647For every issue you raise, assign a tier (Critical / High / Medium / Low / Informational per rubric §2), then **discount it** through the three levers. A raw finding is a hypothesis; the surviving severity is what you report. **Verify each lever yourself in the diff/code — do not inherit the author's framing.**4849- **(a) Reachability + existing mitigations.** Trace the finding to a sink an attacker can actually drive, then check for `rescue` on the immediate caller and a top-level `Pipe::Error`/framework handler before assigning severity. Worked example: the `MessageVerifier`/`MessageEncryptor` `IndexError` "DoS" was fully contained by `SignedStore#verify`, `EncryptedStore#verify_and_decrypt`, and `Pipe::Error` -> Informational/Low, not High. If a mitigation exists, name it and discount.50- **(b) Threat model.** Network-facing vs local dev CLI. A `bin/amber`/CLI command already runs as the operator, so the `encrypt.cr` `system("#{ed} #{file}")` injection is **Low** (operator is the attacker). The same shape on a network endpoint jumps several tiers.51- **(c) Environment gating.** Where does the dangerous branch run? The `error.cr` unescaped `@ex.message` is in the **non-development** branch, so it's a production reflected-XSS (High); a finding that only fires in `development?` is usually Low/Informational because production never executes it.5253Do not accept a severity from the description. Re-derive it from the three levers, citing the lines.5455## 5. Apply the per-type correctness bar5657Grade the diff against the bar for the type you picked in step 3 (rubric §3). The bar is **evidence**, not vibes:5859- **Bugfix:** a spec that was red before the fix and is green now; root-cause not symptom; blast-radius note.60- **Security:** clears all of §3 Security — real+reachable+not-already-mitigated; **complete** fix (the `encrypt.cr` fix that escaped the file but left `$EDITOR` injectable failed this — prefer eliminating the shell via `Process.run(cmd, [args])`); **narrow** fix (no over-broad `rescue`, no loosened auth check); an **adversarial spec** that sends the malicious input and asserts neutralization.61- **Performance:** **provably behavior-identical.** Require the differential edge-case table (dotfiles, trailing slash, empty/extension-only segments) — this is exactly where the `File.extname` router PR diverged while CI stayed green. No table, no merge.62- **Refactor:** existing suite passes **unchanged**; if assertions were edited, it's not a pure refactor — reclassify.63- **Feature/default-change:** tests for the new behavior and its edge cases; docs/changelog updated; description matches code; extra scrutiny per step 7.6465## 6. Discount documented design trade-offs6667Before you flag anything that smells like "why is it done this way," scan [DESIGN_DECISIONS.md](../../DESIGN_DECISIONS.md). If an entry matches, it is **by design** — do not flag it, and reject a PR that "fixes" it without a separate discussion. Common ones the wave tripped on:6869- App defaults live in the **CLI app template**, not core — so "core has no default for X" is often correct, and a pipe not wired into `config/routes.cr.ecr` is dead code.70- `crystal-db`'s pool is already concurrency-safe — don't accept a re-added `Mutex` around checkout.71- `error.cr` is the **non-development** page (dev uses the `exception_page` shard) — review it for the production threat model, not its markup.72- `Process.run(cmd, [args])` (no shell) is the settled shell-out idiom; `headers["X"] ||= value` is the intentional non-clobbering default.73- Extension/content-type lists derive from `Content::TYPE` / `TYPE_EXT_REGEX` — a hand-coded duplicate that can drift *is* flaggable.7475A finding that contradicts an entry needs strong specific evidence (a concrete reachable failure or differential test), not "looks wrong."7677## 7. Decide enhancement vs feature7879Apply the rubric §4 values:8081- **Enhancement** (improves something Amber already does without changing its surface contract) -> review against its matching §3 type bar plus evidence it doesn't change behavior for existing apps. Welcome as a PR.82- **New feature** (new pipe / CLI command / config knob / template default) -> Feature/default-change bar **plus design buy-in**; it should usually have started as an issue/discussion. A drive-by feature PR with no prior agreement is a reason to ask for that discussion first.83- **Default/behavior changes get extra scrutiny.** They change every app silently on upgrade — recall the SecureHeaders PR that shipped HSTS on-by-default (can hard-break HTTP) *and* was inert dead code. Confirm the **semver** call-out in the PR: a default/behavior change is **not** patch-safe (minor at minimum, major if it can break existing requests).8485## 8. Apply AI-PR hygiene8687AI-assisted PRs are welcome (rubric §5) — hold them to the same checklist:8889- [ ] **Description matches the code** — grep the diff against the claims; mismatch is the single most common wave failure.90- [ ] **One concern, scoped diff** — no security-fix-plus-refactor-plus-rename bundles.91- [ ] **Edge-case tests present** — dotfiles, empty/trailing segments, malformed input, non-development env.92- [ ] **No duplication** — check against recently merged PRs; assistants re-propose already-settled patterns (e.g. the `Process.run` no-shell fix).93- [ ] **Learning notes accurate and clean** — `.jules/*`-style notes are fine if correct and contain nothing machine-specific (no absolute local paths, hostnames, usernames, tokens, env dumps).9495## 9. Produce the verdict9697End with one verdict and the evidence behind it. Every claim cites `file:line`.9899- **MERGE** — fits its type bar, levers leave no live high/critical, no design-decision conflict, mechanics clean. Say what you verified.100- **MERGE_WITH_CHANGES** — sound and mergeable after small, named fixes (e.g. add the one missing adversarial spec, wire the pipe into `config/routes.cr.ecr`, drop the machine-specific note). List each required change with its `file:line`.101- **REQUEST_CHANGES** — fails a bar or a lever in a way that needs author rework (incomplete security fix, missing differential table, over-broad `rescue`, description/code mismatch). State the bar it missed and what evidence would clear it.102- **CLOSE** — wrong by design (contradicts a DESIGN_DECISIONS entry without evidence), a non-issue after discounting (the "DoS" that was fully mitigated), a duplicate of settled work, or a feature with no design buy-in that belongs in a discussion. Point to the rubric/decision that settles it.103104Verdict template:105106```107Type: <Bugfix|Security|Performance|Refactor|Feature/default-change>108Mergeability: mergeable=<...> mergeStateStatus=<...> reviewDecision=<...>109Local checks: bin/amber_spec <pass|fail|granite not-run>110Findings:111 - [<Critical|High|Medium|Low|Informational>] <one line> (file:line) — levers applied: <a/b/c result>112Design-decision discounts: <entry or none>113Enhancement vs feature: <which, + semver note if default/behavior change>114AI hygiene: <pass | list gaps>115Verdict: <MERGE|MERGE_WITH_CHANGES|REQUEST_CHANGES|CLOSE>116Required to clear: <named changes with file:line, or n/a>117```