Add close_reason to JSONL format documentation and tests (bd-lxzx)
- Add comprehensive JSONL Issue Schema section to docs/ARCHITECTURE.md documenting all exported fields including close_reason - Add TestCloseReasonRoundTrip test in export_import_test.go to verify close_reason is preserved through JSONL export/import cycle Closes: bd-lxzx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -360,3 +360,88 @@ func TestExportIncludesTombstones(t *testing.T) {
|
|||||||
t.Error("Tombstone not found in JSONL output")
|
t.Error("Tombstone not found in JSONL output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCloseReasonRoundTrip verifies that close_reason is preserved through JSONL export/import (bd-lxzx)
|
||||||
|
func TestCloseReasonRoundTrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "test.db")
|
||||||
|
store := newTestStoreWithPrefix(t, dbPath, "test")
|
||||||
|
|
||||||
|
// Create an issue and close it with a reason
|
||||||
|
issue := &types.Issue{
|
||||||
|
ID: "test-close-reason",
|
||||||
|
Title: "Issue to close",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
||||||
|
t.Fatalf("Failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the issue with a reason
|
||||||
|
closeReason := "Completed: all tests passing"
|
||||||
|
if err := store.CloseIssue(ctx, issue.ID, closeReason, "test-actor"); err != nil {
|
||||||
|
t.Fatalf("Failed to close issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify close_reason was stored
|
||||||
|
closed, err := store.GetIssue(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get closed issue: %v", err)
|
||||||
|
}
|
||||||
|
if closed.CloseReason != closeReason {
|
||||||
|
t.Fatalf("CloseReason not stored: got %q, want %q", closed.CloseReason, closeReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to JSONL
|
||||||
|
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to search issues: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
for _, i := range issues {
|
||||||
|
if err := encoder.Encode(i); err != nil {
|
||||||
|
t.Fatalf("Failed to encode issue: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSONL and verify close_reason is present
|
||||||
|
var decoded types.Issue
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &decoded); err != nil {
|
||||||
|
t.Fatalf("Failed to decode JSONL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.CloseReason != closeReason {
|
||||||
|
t.Errorf("close_reason not preserved in JSONL: got %q, want %q", decoded.CloseReason, closeReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import into a new database and verify close_reason is preserved
|
||||||
|
newDBPath := filepath.Join(tmpDir, "import-test.db")
|
||||||
|
newStore := newTestStoreWithPrefix(t, newDBPath, "test")
|
||||||
|
|
||||||
|
// Re-create the issue in new database (simulating import)
|
||||||
|
decoded.ContentHash = "" // Clear so it gets recomputed
|
||||||
|
if err := newStore.CreateIssue(ctx, &decoded, "test"); err != nil {
|
||||||
|
t.Fatalf("Failed to import issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the imported issue has close_reason
|
||||||
|
imported, err := newStore.GetIssue(ctx, decoded.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get imported issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imported.CloseReason != closeReason {
|
||||||
|
t.Errorf("close_reason not preserved after import: got %q, want %q", imported.CloseReason, closeReason)
|
||||||
|
}
|
||||||
|
if imported.Status != types.StatusClosed {
|
||||||
|
t.Errorf("Status not preserved: got %q, want %q", imported.Status, types.StatusClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -241,6 +241,76 @@ open ──▶ in_progress ──▶ closed
|
|||||||
(reopen)
|
(reopen)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSONL Issue Schema
|
||||||
|
|
||||||
|
Each issue in `.beads/issues.jsonl` is a JSON object with the following fields. Fields marked with `(optional)` use `omitempty` and are excluded when empty/zero.
|
||||||
|
|
||||||
|
**Core Identification:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `id` | string | Unique identifier (e.g., `bd-a1b2`) |
|
||||||
|
|
||||||
|
**Issue Content:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `title` | string | Issue title (required) |
|
||||||
|
| `description` | string | Detailed description (optional) |
|
||||||
|
| `design` | string | Design notes (optional) |
|
||||||
|
| `acceptance_criteria` | string | Acceptance criteria (optional) |
|
||||||
|
| `notes` | string | Additional notes (optional) |
|
||||||
|
|
||||||
|
**Status & Workflow:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `status` | string | Current status: `open`, `in_progress`, `blocked`, `deferred`, `closed`, `tombstone`, `pinned`, `hooked` (optional, defaults to `open`) |
|
||||||
|
| `priority` | int | Priority 0-4 where 0=critical, 4=backlog |
|
||||||
|
| `issue_type` | string | Type: `bug`, `feature`, `task`, `epic`, `chore`, `message`, `merge-request`, `molecule`, `gate`, `agent`, `role`, `convoy` (optional, defaults to `task`) |
|
||||||
|
|
||||||
|
**Assignment:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `assignee` | string | Assigned user/agent (optional) |
|
||||||
|
| `estimated_minutes` | int | Time estimate in minutes (optional) |
|
||||||
|
|
||||||
|
**Timestamps:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `created_at` | RFC3339 | When issue was created |
|
||||||
|
| `created_by` | string | Who created the issue (optional) |
|
||||||
|
| `updated_at` | RFC3339 | Last modification time |
|
||||||
|
| `closed_at` | RFC3339 | When issue was closed (optional, set when status=closed) |
|
||||||
|
| `close_reason` | string | Reason provided when closing (optional) |
|
||||||
|
|
||||||
|
**External Integration:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `external_ref` | string | External reference (e.g., `gh-9`, `jira-ABC`) (optional) |
|
||||||
|
|
||||||
|
**Relational Data:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `labels` | []string | Tags attached to the issue (optional) |
|
||||||
|
| `dependencies` | []Dependency | Relationships to other issues (optional) |
|
||||||
|
| `comments` | []Comment | Discussion comments (optional) |
|
||||||
|
|
||||||
|
**Tombstone Fields (soft-delete):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `deleted_at` | RFC3339 | When deleted (optional, set when status=tombstone) |
|
||||||
|
| `deleted_by` | string | Who deleted (optional) |
|
||||||
|
| `delete_reason` | string | Why deleted (optional) |
|
||||||
|
| `original_type` | string | Issue type before deletion (optional) |
|
||||||
|
|
||||||
|
**Note:** Fields with `json:"-"` tags (like `content_hash`, `source_repo`, `id_prefix`) are internal and never exported to JSONL.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user