From da550f0e1b5034339187aa9ed8767896fc8d3e97 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 30 Dec 2025 17:03:17 -0800 Subject: [PATCH] Add close_reason to JSONL format documentation and tests (bd-lxzx) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- cmd/bd/export_import_test.go | 85 ++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 70 +++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/cmd/bd/export_import_test.go b/cmd/bd/export_import_test.go index 91b13619..6b54c538 100644 --- a/cmd/bd/export_import_test.go +++ b/cmd/bd/export_import_test.go @@ -360,3 +360,88 @@ func TestExportIncludesTombstones(t *testing.T) { 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) + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ce2e4ff8..ca8d3579 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -241,6 +241,76 @@ open ──▶ in_progress ──▶ closed (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 ```