Removing intermediate files
CI / test (pull_request) Successful in 18s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 21s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 24s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 41s

This commit is contained in:
Rodin
2026-05-19 02:15:06 +00:00
parent 9673a9d53c
commit e560781c87
19 changed files with 0 additions and 1806 deletions
-82
View File
@@ -1,82 +0,0 @@
# Design: doc-map input for path-scoped design doc injection (Issue #137)
## Problem
review-bot can inject context via `patterns-repo` (external VCS repos) and `conventions-file`
(a single file from the reviewed repo). There is no mechanism to inject local repo documentation
files scoped to the paths changed in a PR.
First consumer: `grgl/gargoyle#778` needs a "doc adherence" reviewer that checks code against the
module's governing design doc, without injecting every doc in the tree.
## Approach
### New: `doc-map` input
A `.review-bot/doc-map.yml` config file in the reviewed repo maps source path globs to governing
design docs. review-bot reads the map, intersects it with changed PR paths, and injects only the
relevant docs into the system prompt.
### Config format
```yaml
mappings:
- paths:
- "lib/gargoyle/engine/signal_risk/**"
docs:
- docs/domain/contexts/risk/risk-controls.md
- paths:
- "lib/gargoyle/trading/**"
docs:
- docs/domain/contexts/trading/
```
- `paths` — glob patterns (including `**`) matched against changed file paths in the PR
- `docs` — file paths or directory paths (all `.md` files under a directory) to inject
- Docs are deduplicated across mappings
### Architecture
| Component | Description |
|-----------|-------------|
| `review/docmap.go` | YAML parsing, glob matching with `**` support, doc loading via VCS |
| `cmd/review-bot/main.go` | Step 6c: parses config, intersects with changed files, calls LoadMatchingDocs |
| `budget/budget.go` | New `DesignDocs` section — injected after Conventions in system prompt |
| `action.yml` | `doc-map` and `doc-map-max-bytes` inputs, wired to `DOC_MAP_FILE`/`DOC_MAP_MAX_BYTES` |
### Doc file loading
- The `doc-map` YAML file is read from the local workspace (like `system-prompt-file`).
- Doc files listed in the config are fetched via VCS API (same as `conventions-file`),
enabling them to be loaded from any branch without a local checkout.
- `GetAllFilesInPath` is tried first; if it returns files, they are treated as a directory listing.
If it returns empty, `GetFileContent` is tried as a fallback (single file).
### Glob matching
`**` is implemented by splitting patterns and paths on `/`, then matching segment-by-segment.
A `**` segment consumes zero or more path segments (not just one level like `*`).
### Budget integration
`DesignDocs` is added to `budget.Sections` between `Conventions` and `FileContext`.
Trim order: Patterns → Conventions → DesignDocs → FileContext → Diff.
Design docs appear in the system prompt under `## Design Documents`.
### Context size guard
Default: 100 KB. Configurable via `--doc-map-max-bytes` / `DOC_MAP_MAX_BYTES`.
Truncation is noted inline with a `⚠️` message.
## Error handling
| Situation | Behavior |
|-----------|----------|
| `--doc-map` file not found | Fatal error (like `--system-prompt-file`) |
| `--doc-map` file invalid YAML | Fatal error with descriptive message |
| Unknown YAML keys | Log warning, continue |
| Doc file not found in VCS | Log warning, skip |
| Doc directory empty or no `.md` files | Log debug, skip |
| Total size exceeds limit | Truncate with notice, log warning |
| No changed paths match any mapping | No docs injected, review runs normally |
| `paths` or `docs` list empty in a mapping | Skip that mapping |
-334
View File
@@ -1,334 +0,0 @@
# Design: Role-based Review Personas (Issue #51)
> **Note:** This design was revised during implementation to use JSON instead of YAML
> to maintain the repository's zero-external-dependencies convention. All persona
> files use JSON format. See "Design Revision" section at the end for details.
## Problem
Current review-bot performs generic code review. Every reviewer (regardless of `reviewer-name`) uses the same base prompt and evaluates the same concerns. This leads to:
1. **Redundancy** — Two reviewers (e.g., GPT + Claude twins) often flag identical issues
2. **Gaps** — Generic reviewers miss specialized concerns (security, domain logic, architecture)
3. **Noise** — NITs about style mixed with critical security findings
4. **No ownership** — Findings lack clear domain attribution
## Constraints
- Must work with existing CLI flags and CI workflow patterns
- Must not break backwards compatibility (existing configs still work)
- Must integrate cleanly with the budget system (personas add to context)
- Multiple personas running in parallel must not interfere with each other
- Each persona must have clear scope boundaries (no duplication)
## Proposed Approach
### 1. Persona Definition
A persona is a named review role with:
- **Identity** — Who am I? What's my expertise?
- **Focus** — What do I look for?
- **Scope boundaries** — What do I explicitly NOT comment on?
- **Severity calibration** — What counts as MAJOR/MINOR/NIT for MY domain?
Personas are defined in JSON files that can live:
1. In the pattern repos (shared across projects)
2. In the target repo (project-specific personas)
3. Inline via a new `--persona-file` flag (JSON format)
### 2. Persona File Format
```json
# .review/personas/security.yaml
name: security
display_name: Security Specialist
model_preference: opus # optional hint for expensive analysis
identity: |
You are a security specialist reviewing code for vulnerabilities.
Your expertise: OWASP Top 10, injection attacks, auth/authz, secrets management,
event sourcing security (replay attacks, event injection).
focus:
- Injection attacks (SQL, command, path traversal, template)
- Authentication and authorization gaps
- Secrets exposure (hardcoded credentials, tokens in logs)
- Input validation (unsanitized input, unsafe deserialization)
- Race conditions with security implications
- Event sourcing attack vectors
ignore:
- Code style and naming conventions
- Performance (unless security-related)
- Documentation
- General code quality
- Test coverage
severity:
critical: "Remote code execution, auth bypass, data exfiltration"
major: "Privilege escalation, information disclosure, DoS"
minor: "Missing rate limiting, verbose errors"
nit: "Theoretical risk with low exploitability"
output_format: |
For each finding:
- Severity: [CRITICAL|MAJOR|MINOR|NIT]
- Attack vector: How could this be exploited?
- Evidence: Code snippet showing the vulnerability
- Recommendation: Specific fix
```
### 3. New CLI Flags
```
--persona-file PATH Path to persona JSON file (local or in repo)
--persona NAME Built-in persona name (security, architect, domain)
```
Either flag sets the persona. If neither is provided, behavior is unchanged (generic review).
### 4. Prompt Assembly
Current flow:
```
SystemBase → Patterns → Conventions → [LLM]
```
New flow with persona:
```
PersonaPrompt (from YAML) → Patterns (filtered?) → Conventions → [LLM]
```
The persona's identity/focus/ignore/severity sections become the system prompt, replacing the generic "You are an expert code reviewer" base.
### 5. Built-in Personas
Ship with these built-in personas (loadable via `--persona NAME`):
| Name | Focus |
|------|-------|
| `security` | Vulnerabilities, auth, secrets |
| `architect` | Patterns, consistency, design |
| `domain` | Business logic (requires repo-specific config) |
| `docs` | Documentation, API clarity |
Built-in personas live in `review/personas/` as embedded Go assets or YAML shipped with the binary.
### 6. CI Workflow Integration
Single persona:
```yaml
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: security
persona: security
...
```
Multiple personas (parallel jobs):
```yaml
jobs:
review:
strategy:
matrix:
include:
- name: security
persona: security
- name: architect
persona: architect
steps:
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: ${{ matrix.name }}
persona: ${{ matrix.persona }}
```
Custom persona from repo:
```yaml
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: trading
persona-file: .review/personas/trading.yaml
```
### 7. Persona + Patterns Interaction
Some personas benefit from filtered patterns:
- Security → only security-related patterns
- Architect → all patterns (structural focus)
- Domain → domain docs, not language patterns
For v1, keep it simple: all patterns are included regardless of persona. Future enhancement could add `patterns_filter` to persona YAML.
### 8. Output Format Changes
Persona name appears in the review header:
```markdown
# Security Review
## Summary
No critical vulnerabilities found in this change.
## Findings
| # | Severity | File | Line | Finding |
...
## Recommendation
**APPROVE** — No security-relevant issues detected.
---
*Review by security*
<!-- review-bot:security -->
```
## State/Data Model
### Persona struct
```go
// review/persona.go
type Persona struct {
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
ModelPref string `yaml:"model_preference,omitempty"`
Identity string `yaml:"identity"`
Focus []string `yaml:"focus"`
Ignore []string `yaml:"ignore"`
Severity Severity `yaml:"severity"`
OutputFormat string `yaml:"output_format,omitempty"`
}
type Severity struct {
Critical string `yaml:"critical"`
Major string `yaml:"major"`
Minor string `yaml:"minor"`
Nit string `yaml:"nit"`
}
```
### Loading precedence
1. `--persona-file PATH` → load from local file system
2. `--persona NAME` → load from embedded built-ins
3. Neither → use generic system prompt (current behavior)
## Error Cases
| Error | Handling |
|-------|----------|
| Persona file not found | Fatal exit with clear message |
| Invalid YAML in persona file | Fatal exit with parse error |
| Both `--persona` and `--persona-file` specified | Fatal exit: mutually exclusive |
| Unknown built-in persona name | Fatal exit with list of valid names |
| Empty identity in persona | Warning, fall back to generic prompt |
## Edge Cases
- **Empty focus list**: Valid — persona relies on identity alone
- **Empty ignore list**: Valid — no explicit scope exclusions
- **No severity section**: Use default MAJOR/MINOR/NIT definitions
- **Model preference set but budget insufficient**: Ignore preference, log warning
- **Persona file in pattern repo**: Fetch like other pattern files
## Testing Strategy
### Unit tests
- `persona_test.go`: Parse valid/invalid YAML, validate required fields
- `prompt_test.go`: Verify persona prompt assembly
- Integration with budget: persona prompts count toward token limit
### Integration tests
- End-to-end with `--persona security` (built-in)
- End-to-end with `--persona-file custom.yaml`
- Backwards compatibility: no flags = generic behavior
### Manual verification
- Run security persona on a PR with obvious vulnerability
- Verify security persona ignores style issues
- Verify non-security persona doesn't flag security issues
## Implementation Phases
### Phase 1: Persona types and loading
- [ ] `review/persona.go`: Persona struct + YAML parsing
- [ ] `review/persona_test.go`: Unit tests
- [ ] Embed built-in personas in binary
- [ ] Compiles clean, tests pass
### Phase 2: Prompt generation
- [ ] `review/prompt.go`: `BuildPersonaPrompt(p Persona) string`
- [ ] Modify `BuildSystemBase()` to accept optional persona
- [ ] Integrate persona prompt with budget system
- [ ] Tests for prompt assembly
### Phase 3: CLI integration
- [ ] Add `--persona` and `--persona-file` flags
- [ ] Flag validation (mutually exclusive, valid names)
- [ ] Load persona based on flags
- [ ] Pass persona to prompt builder
### Phase 4: Action integration
- [ ] Add `persona` and `persona-file` inputs to action.yml
- [ ] Update README with persona examples
- [ ] End-to-end CI test
### Phase 5: Built-in personas
- [ ] `security.yaml` built-in
- [ ] `architect.yaml` built-in
- [ ] `docs.yaml` built-in
- [ ] Document each persona's focus
## Open Questions
1. **Persona file location in repo**: Should we support `--persona-file .review/security.yaml` where the file is fetched from the PR's repo (like conventions)? This adds complexity but enables project-specific personas without action changes.
2. **Model preference enforcement**: If persona specifies `model_preference: opus` but the action uses a different model, should we warn? Override? Ignore? Current thinking: log warning, use the specified model (user controls model via action input).
3. **Severity override output**: If persona defines custom severity levels (CRITICAL), should the JSON output include them, or map back to standard MAJOR/MINOR/NIT? Current thinking: keep standard output format, use severity calibration only for prompt guidance.
## Completion Checklist
1. Persona struct matches YAML schema exactly?
2. Built-in personas embedded in binary (not external files)?
3. `--persona` and `--persona-file` are mutually exclusive?
4. Unknown persona name produces clear error with valid options?
5. Empty persona file fields have sensible defaults?
6. Persona prompt integrates with budget system (token counting)?
7. Backwards compatibility: no flags = current behavior?
8. Review header shows persona display name?
9. Sentinel still uses reviewer-name (not persona name)?
10. Unit tests cover parse errors, missing fields, valid YAML?
## Design Review Findings (Self-Review)
### Finding 1: Severity Mapping
The persona YAML allows `critical` severity, but the LLM output parser (`review/parser.go`) only accepts MAJOR/MINOR/NIT.
**Resolution:** Keep standard output format. Persona severity section is ONLY for calibrating the LLM's judgment (prompt guidance). Output must still use MAJOR/MINOR/NIT. Document this clearly in persona format docs.
### Finding 2: Embedding Built-in Personas
Go doesn't natively embed YAML. Must use `//go:embed` directive (Go 1.16+).
**Resolution:** Create `review/personas/` directory with YAML files and use:
```go
//go:embed personas/*.yaml
var embeddedPersonas embed.FS
```
### Finding 3: display_name vs reviewer-name
Design says header shows "persona display name" but sentinel uses "reviewer-name". This is correct - they serve different purposes:
- `display_name` → human-readable header ("Security Specialist Review")
- `reviewer-name` → machine sentinel for cleanup (`<!-- review-bot:security -->`)
When persona is used, `display_name` takes precedence for the header title, but `reviewer-name` (CLI flag) is still used for the sentinel.
## Design Revision: YAML with gopkg.in/yaml.v3
**Decision:** Add `gopkg.in/yaml.v3` as a dependency.
YAML is preferred over JSON for persona files because:
- Multi-line strings are cleaner (no escaping quotes in identity/focus text)
- Comments are supported for documentation
- More human-readable for complex persona definitions
The implementation supports both YAML (`.yaml`, `.yml`) and JSON (`.json`) for backwards compatibility, with YAML as the default for built-in personas.
-87
View File
@@ -1,87 +0,0 @@
# Design: YAML Support for Persona Files (#57)
## Problem
JSON is awkward for persona files that contain multi-line text (identity, severity descriptions). YAML supports cleaner multi-line strings and comments, improving readability and maintainability.
## Constraints
- Backwards compatibility: existing JSON personas must continue to work
- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)
- Consistency: use `.yaml` extension (not `.yml`)
- Library: use `github.com/goccy/go-yaml` v1.16.0+ (approved in CONVENTIONS.md); we implement custom AST-based depth/node-count checks for precise alias-aware validation
## Proposed Approach
1. **Update `parsePersona`** to detect format from file extension
2. **Add YAML parsing** with explicit depth limit (defense in depth)
3. **Keep JSON as fallback** for files without `.yaml`/`.yml` extension
4. **Convert built-in personas** to YAML format
5. **Update embed directive** to include both formats
### File Extension Detection
```go
func parsePersona(data []byte, source string) (*Persona, error) {
isYAML := strings.HasSuffix(source, ".yaml") || strings.HasSuffix(source, ".yml")
if isYAML {
return parseYAML(data, source)
}
return parseJSON(data, source)
}
```
### YAML Parsing with Depth Protection
We implement a custom AST-based depth/node-count walk (`checkYAMLDepth` in
`review/persona.go`) rather than relying on library decoder options. Key design
decisions:
- **Library:** `github.com/goccy/go-yaml` with `ast.Node`-based traversal
- **Dual-map tracking:** `validated` (depth-aware short-circuit) + `visiting` (cycle detection)
- **Node-count limit:** Conservative overcounting bounds total validation work
- **Alias-aware depth:** Aliases increment depth and are re-checked when encountered at greater depths
See `review/persona.go:checkYAMLDepth` for the authoritative implementation.
## State/Data Model
No new state. Same `Persona` struct, just different parsing.
## Error Cases
| Error | Handling |
|-------|----------|
| Invalid YAML syntax | Return parse error with source file |
| Deeply nested YAML | Custom AST walk (`checkYAMLDepth`) rejects before decode |
| Unknown extension | Fall back to JSON parsing |
| Missing required fields | Validation rejects after parse |
## Edge Cases
- File with `.json` extension but YAML content → JSON parse fails, user sees error
- File with no extension → defaults to JSON
- Embedded persona reference like `builtin:security` → detect by embed path (`personas/X.yaml`)
## Testing Strategy
1. Unit tests for YAML parsing (valid, invalid, deeply nested)
2. Unit tests for extension detection
3. Integration test for built-in personas (now YAML)
4. Backwards compat test: verify JSON still works for external files
## Completion Checklist
1. [ ] `go-yaml` dependency added at v1.16.0+
2. [ ] Extension detection uses case-insensitive comparison
3. [ ] YAML parse errors include source file name
4. [ ] JSON parsing still works for `.json` files
5. [ ] Built-in personas converted to YAML with readable multi-line strings
6. [ ] Embed directive updated to include `*.yaml`
7. [ ] Test for deeply nested YAML rejection
8. [ ] All existing tests pass
## Open Questions
- Should we support both `.yaml` AND `.yml`? Issue says `.yaml` only for consistency, but some users expect `.yml`. **Decision:** Support both for reading, recommend `.yaml` in docs.
- Should we add a "format" field to detect mismatched extension/content? **Decision:** No, keep it simple. Extension determines format.