Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,28 +105,16 @@ prism fetch <PR_URL> --format text # Raw PR data as text

```json
{
"provider": "github",
"pull_request": {
"provider": "github",
"repository": "owner/repo",
"id": "123",
"title": "Add retry handling for payment API",
"author": "example",
"source_branch": "feature/payment-retry",
"target_branch": "main",
"description": "Adds retry logic for transient payment API failures"
"url": "https://github.com/owner/repo/pull/123"
},
"changed_files": [
{
"path": "internal/payment/client.go",
"status": "modified",
"additions": 45,
"deletions": 3,
"language": "Go",
"is_test": false,
"is_config": false,
"is_generated": false
}
],
"analysis": {
"change_type": "feature",
"risk_level": "medium",
Expand All @@ -145,7 +133,16 @@ prism fetch <PR_URL> --format text # Raw PR data as text
"No test files included in this change"
],
"summary": "feature: Add retry handling for payment API (1 file changed, +45/-3)"
}
},
"changed_files": [
{
"path": "internal/payment/client.go",
"status": "modified",
"additions": 45,
"deletions": 3,
"language": "Go"
}
]
}
```

Expand All @@ -166,6 +163,7 @@ prism fetch <PR_URL> --format text # Raw PR data as text
| Author | example |
| Branch | feature/payment-retry -> main |
| Provider | github |
| URL | https://github.com/owner/repo/pull/123 |

## Analysis

Expand Down Expand Up @@ -435,8 +433,9 @@ make clean # Remove bin/

- **v0.1.0** — GitHub provider, analyze/prompt/fetch commands, JSON/Markdown/text output, light/detailed/cross modes, config/lang/template support, exit codes ✅
- **v0.2.0** — Provider plugin architecture, `--provider` flag, AWS CodeCommit support via plugin ✅
- **v0.3.0** — Policy files, custom review axes, project-specific rules
- **v0.4.0+** — Review policy as code, SARIF output, metrics, IDE/CI integration
- **v0.3.0** — Public library API (`pkg/prism`), CLI uses `pkg/prism` internally, foundation for `prism-api` (HTTP service)
- **v0.4.0** — Policy files, custom review axes, project-specific rules
- **v0.5.0+** — SARIF output, metrics, IDE/CI integration

## Releases

Expand Down
87 changes: 4 additions & 83 deletions cmd/prism/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/hidetzu/prism/internal/domain"
"github.com/hidetzu/prism/internal/formatter"
"github.com/hidetzu/prism/internal/provider"
ghprovider "github.com/hidetzu/prism/internal/provider/github"
"github.com/hidetzu/prism/internal/usecase"
Expand Down Expand Up @@ -158,82 +156,10 @@ func TestCLIHelpContainsCommands(t *testing.T) {
}
}

// TestUsecaseIntegrationAnalyzeJSON tests the full pipeline through usecase layer
// with a mock HTTP server, verifying JSON output structure.
func TestUsecaseIntegrationAnalyzeJSON(t *testing.T) {
server := setupGitHubMock(t)
t.Cleanup(server.Close)

// Use provider with mock server directly.
ghprov := newTestProvider(t, server)
ref, err := ghprov.Parse("https://github.com/owner/repo/pull/1")
if err != nil {
t.Fatalf("Parse: %v", err)
}

var buf bytes.Buffer
err = usecaseAnalyze(t, ghprov, ref, "json", &buf)
if err != nil {
t.Fatalf("Analyze: %v", err)
}

var out formatter.Output
if err := json.Unmarshal(buf.Bytes(), &out); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if out.Provider != "github" {
t.Errorf("provider = %q", out.Provider)
}
if out.PullRequest.Title != "Fix login bug" {
t.Errorf("title = %q", out.PullRequest.Title)
}
if out.Analysis.ChangeType != "bugfix" {
t.Errorf("change_type = %q, want bugfix", out.Analysis.ChangeType)
}
if len(out.ChangedFiles) != 2 {
t.Errorf("changed_files = %d, want 2", len(out.ChangedFiles))
}
}

func TestUsecaseIntegrationAnalyzeMarkdown(t *testing.T) {
server := setupGitHubMock(t)
t.Cleanup(server.Close)

ghprov := newTestProvider(t, server)
ref, _ := ghprov.Parse("https://github.com/owner/repo/pull/1")

var buf bytes.Buffer
if err := usecaseAnalyze(t, ghprov, ref, "markdown", &buf); err != nil {
t.Fatalf("Analyze markdown: %v", err)
}
out := buf.String()
if !strings.Contains(out, "# Fix login bug") {
t.Error("markdown missing title")
}
if !strings.Contains(out, "bugfix") {
t.Error("markdown missing change type")
}
}

func TestUsecaseIntegrationAnalyzeText(t *testing.T) {
server := setupGitHubMock(t)
t.Cleanup(server.Close)

ghprov := newTestProvider(t, server)
ref, _ := ghprov.Parse("https://github.com/owner/repo/pull/1")

var buf bytes.Buffer
if err := usecaseAnalyze(t, ghprov, ref, "text", &buf); err != nil {
t.Fatalf("Analyze text: %v", err)
}
out := buf.String()
if !strings.Contains(out, "Fix login bug") {
t.Error("text missing title")
}
if !strings.Contains(out, "bugfix") {
t.Error("text missing change type")
}
}
// Note: The analyze pipeline (provider → classifier → analyzer → formatter) is
// covered by unit tests in pkg/prism, internal/formatter, internal/classifier,
// and internal/analyzer. The integration tests below focus on prompt and fetch
// use cases which still go through internal/usecase.

func TestUsecaseIntegrationPromptLight(t *testing.T) {
server := setupGitHubMock(t)
Expand Down Expand Up @@ -365,11 +291,6 @@ func (p *ghProviderForTest) FetchPullRequest(ctx context.Context, ref domain.PRR
return prov.FetchPullRequest(ctx, ref)
}

func usecaseAnalyze(t *testing.T, p provider.Provider, ref domain.PRRef, format string, buf *bytes.Buffer) error {
t.Helper()
return usecase.Analyze(context.Background(), p, ref, usecase.AnalyzeOptions{Format: format}, buf)
}

func usecasePrompt(t *testing.T, p provider.Provider, ref domain.PRRef, mode, format, lang string, buf *bytes.Buffer) error {
t.Helper()
return usecase.Prompt(context.Background(), p, ref, usecase.PromptOptions{Mode: mode, Format: format, Lang: lang}, buf)
Expand Down
45 changes: 34 additions & 11 deletions cmd/prism/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (

"github.com/hidetzu/prism/internal/config"
"github.com/hidetzu/prism/internal/domain"
"github.com/hidetzu/prism/internal/formatter"
"github.com/hidetzu/prism/internal/provider"
"github.com/hidetzu/prism/internal/usecase"
"github.com/hidetzu/prism/pkg/prism"
)

const version = "0.2.0"
const version = "0.3.0-dev"

// Exit codes as defined in docs/spec.md.
const (
Expand All @@ -33,9 +35,13 @@ func main() {

func exitCode(err error) int {
switch {
case errors.Is(err, domain.ErrInvalidArgs):
case errors.Is(err, domain.ErrInvalidArgs),
errors.Is(err, prism.ErrInvalidInput),
errors.Is(err, prism.ErrUnsupportedProvider):
return ExitInvalidArgs
case errors.Is(err, domain.ErrProvider):
case errors.Is(err, domain.ErrProvider),
errors.Is(err, prism.ErrAuthRequired),
errors.Is(err, prism.ErrUpstreamFailure):
return ExitProviderError
case errors.Is(err, domain.ErrAnalysis):
return ExitAnalysisError
Expand Down Expand Up @@ -134,19 +140,36 @@ func runAnalyze(cmd *cobra.Command, args []string) error {
return fmt.Errorf("load config: %w", err)
}

p, ref, err := resolveProvider(cmd, cfg, args[0])
if err != nil {
return err
}

format, _ := cmd.Flags().GetString("format")
if format == "" {
format = cfg.DefaultFormat
}
if format == "" {
format = "json"
}
if format != "json" && format != "markdown" && format != "text" {
return fmt.Errorf("%w: invalid format %q: must be json, markdown, or text", domain.ErrInvalidArgs, format)
}

return usecase.Analyze(cmd.Context(), p, ref, usecase.AnalyzeOptions{
Format: format,
}, os.Stdout)
providerName, _ := cmd.Flags().GetString("provider")

result, err := prism.Analyze(cmd.Context(), prism.AnalyzeOptions{
Provider: providerName,
PRURL: args[0],
GitHubToken: cfg.GitHubToken,
})
if err != nil {
return err
}

switch format {
case "markdown":
return formatter.FormatMarkdown(os.Stdout, result)
case "text":
return formatter.FormatText(os.Stdout, result)
default:
return formatter.FormatJSON(os.Stdout, result)
}
}

func runPrompt(cmd *cobra.Command, args []string) error {
Expand Down
37 changes: 20 additions & 17 deletions docs/adr/0002-public-api-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,19 +174,19 @@ The `AnalyzeOptions` struct is designed to be **additive-only**: new optional fi
- **Formatter functions** — callers work with `Result` as structured data, not serialized strings. If string output is needed, callers format `Result` themselves.
- **Prompt templates** — templates are internal. Custom templates are still supported via `--template` at the CLI level, which reads from a file path.

### 4. Differences from the CLI JSON schema
### 4. CLI JSON output and `Result` are byte-identical

`Result` is **structurally similar** to the CLI JSON output (`docs/json-schema.md`) but not identical. The differences are intentional:
As of Phase 2 (v0.3.0), the CLI JSON output is produced by serializing `pkg/prism.Result` directly. The two are guaranteed to be byte-identical, verified by golden tests in `internal/formatter/testdata/`.

| Field | CLI JSON | `pkg/prism.Result` | Reason |
|-------|----------|---------------------|--------|
| `provider` | top-level | `pull_request.provider` | Library consumers treat the PR as a self-contained object; provider belongs with PR metadata |
The Phase 1 design intentionally diverged from the v0.2.x CLI JSON schema in three places:

| Field | v0.2.x CLI JSON | `pkg/prism.Result` (v0.3.0+) | Reason |
|-------|------------------|------------------------------|--------|
| `provider` | top-level | `pull_request.provider` | Provider belongs with PR metadata; library consumers treat the PR as a self-contained object |
| `pull_request.description` | included | **excluded** | Descriptions can be large and are rarely needed by programmatic consumers; reduces response size |
| `pull_request.url` | not present | **included** | Avoids requiring consumers to reconstruct the URL from owner/repo/id |

These differences will be unified in **Phase 2** when the CLI is refactored to use `pkg/prism` internally. At that point, the CLI JSON schema will align with `pkg/prism.Result` (a potential breaking change to CLI output, to be announced in release notes).

Until then, consumers who need the exact CLI JSON shape should use the CLI binary; consumers who want the library API should use `pkg/prism`.
In v0.3.0, the CLI JSON schema is updated to match `pkg/prism.Result`. This is a **breaking change** to the CLI JSON output, documented in the v0.3.0 release notes.

### 5. Compatibility guarantees

Expand Down Expand Up @@ -264,18 +264,21 @@ Accepted. Minimal public surface, maximum internal freedom, clean consumer story
3. Add unit tests for `pkg/prism` covering validation and error categorization
4. Document the public API in `docs/public-api.md` or README

### Phase 2 (subsequent PR)
### Phase 2 (completed in v0.3.0)

5. Refactor `cmd/prism analyze` to use `pkg/prism.Analyze()` internally ✓
6. Rewrite `internal/formatter` to take `prism.Result` instead of domain types ✓
7. Update CLI JSON schema to match `pkg/prism.Result` (breaking change documented in release notes) ✓

### Phase 2 deferred items

5. Refactor `cmd/prism` to use `pkg/prism` internally
- `analyze` JSON format: call `pkg/prism.Analyze()` and `json.Marshal(result)`
- `analyze` markdown/text format: either reuse existing `internal/formatter` (taking `Result`) or keep current path
- `prompt` command: switch to `pkg/prism.Prompt()`
6. Verify CLI JSON output matches `pkg/prism.Result` JSON exactly via a golden test
- `cmd/prism prompt` still uses `internal/usecase` because `pkg/prism.Prompt()` returns a plain string and does not yet cover `--format json|markdown` or `--template`. Future work: extend `pkg/prism` with a `RenderPrompt` function returning a structured bundle.
- `cmd/prism fetch` still uses `internal/usecase` since it bypasses analysis entirely.

### Phase 3 (after CLI refactor)
### Phase 3

7. Create `prism-api` repository
8. Tag prism v0.3.0 once the public API is validated
8. Create `prism-api` repository
9. Tag prism v0.3.0 once the public API is validated

---

Expand Down
16 changes: 10 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ flowchart LR
B --> C[PullRequest<br/>domain model]
C --> D[Classifier]
D --> E[Analyzer]
E --> F[AnalysisResult]
E --> F[prism.Result]
F --> G{Output}
G -->|analyze| H[Formatter<br/>JSON / Markdown]
G -->|analyze| H[Formatter<br/>JSON / Markdown / Text]
G -->|prompt| I[Prompt Renderer<br/>light / detailed / cross]
```

The `analyze` flow is exposed as `pkg/prism.Analyze()` and used by both the CLI and external library consumers. The `prompt` flow currently goes through `internal/usecase`.

## Package Responsibilities

### `pkg/prism`
Expand All @@ -71,7 +73,7 @@ Exposes `Analyze` and `Prompt` functions, stable input/output types (`AnalyzeOpt

Consumers:

- **cmd/prism** — the CLI (refactor to use pkg/prism is Phase 2)
- **cmd/prism** — the CLI (`analyze` command uses `pkg/prism.Analyze()` internally)
- **prism-api** — HTTP service (planned)
- **Editor / IDE plugins** — library consumers
- **CI / automation tools** — library consumers
Expand All @@ -80,7 +82,7 @@ See [ADR-0002](adr/0002-public-api-boundary.md) for the design rationale and com

### `cmd/prism`

CLI entrypoint. Parses arguments, resolves configuration, and delegates to use cases. Should remain thin.
CLI entrypoint. Parses arguments, loads configuration, and delegates to either `pkg/prism` (for `analyze`) or `internal/usecase` (for `prompt` and `fetch`). Should remain thin.

### `internal/domain`

Expand Down Expand Up @@ -123,15 +125,17 @@ Estimates risk level and suggests review axes based on classification results an

### `internal/formatter`

Serializes `AnalysisResult` into output formats (JSON, Markdown, text).
Serializes `pkg/prism.Result` into output formats (JSON, Markdown, text). Used by `cmd/prism analyze` to render the result returned by `pkg/prism.Analyze()`. The CLI JSON output is byte-identical to `pkg/prism.Result` JSON, verified by golden tests.

### `internal/prompt`

Renders `PromptBundle` for each review mode using templates.

### `internal/usecase`

Orchestrates the pipeline: fetch → classify → analyze → format/render. Each use case corresponds to a CLI command.
Orchestrates the pipeline for `prism prompt` and `prism fetch` commands. The `analyze` use case has been replaced by `pkg/prism.Analyze()` (the CLI calls `pkg/prism` directly and formats the result via `internal/formatter`).

Deferred to a future phase: extending `pkg/prism.Prompt` to cover the prompt CLI features (`--format json|markdown`, `--template`), so that prompt and fetch can also flow through `pkg/prism`.

## Design Principles

Expand Down
Loading
Loading