From f79e636000e149937e154288e1e77645284f215a Mon Sep 17 00:00:00 2001 From: beads/crew/dave Date: Sat, 10 Jan 2026 20:26:57 -0800 Subject: [PATCH] feat: consolidate schema changes from crew directories Merges schema additions from crew/fang, crew/giles, crew/grip, and crew/wolf: - crystallizes: bool field for work economics (compounds vs evaporates) - work_type: WorkType field for assignment model (mutex vs open_competition) - source_system: string field for federation adapter tracking - quality_score: *float32 for aggregate quality (0.0-1.0) - delegated-from: new dependency type for work delegation chains Migrations properly sequenced as 037-040 (after existing 036 owner_column). Also fixes test compilation errors for removed TypeRig and IsBuiltIn references. Co-Authored-By: Claude Opus 4.5 Executed-By: beads/crew/dave Rig: beads Role: crew --- internal/storage/sqlite/labels.go | 2 +- internal/storage/sqlite/migrations.go | 8 ++++ .../migrations/037_crystallizes_column.go | 36 ++++++++++++++++ .../sqlite/migrations/038_work_type_column.go | 34 +++++++++++++++ .../migrations/039_source_system_column.go | 33 ++++++++++++++ .../migrations/040_quality_score_column.go | 34 +++++++++++++++ internal/storage/sqlite/schema.go | 8 ++++ internal/types/types.go | 43 +++++++++++++++++-- internal/types/types_test.go | 41 ------------------ 9 files changed, 193 insertions(+), 46 deletions(-) create mode 100644 internal/storage/sqlite/migrations/037_crystallizes_column.go create mode 100644 internal/storage/sqlite/migrations/038_work_type_column.go create mode 100644 internal/storage/sqlite/migrations/039_source_system_column.go create mode 100644 internal/storage/sqlite/migrations/040_quality_score_column.go diff --git a/internal/storage/sqlite/labels.go b/internal/storage/sqlite/labels.go index baa1d035..25b841af 100644 --- a/internal/storage/sqlite/labels.go +++ b/internal/storage/sqlite/labels.go @@ -157,7 +157,7 @@ func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]* rows, err := s.db.QueryContext(ctx, ` SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes, i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes, - i.created_at, i.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason, + i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason, i.deleted_at, i.deleted_by, i.delete_reason, i.original_type, i.sender, i.ephemeral, i.pinned, i.is_template, i.await_type, i.await_id, i.timeout_ns, i.waiters diff --git a/internal/storage/sqlite/migrations.go b/internal/storage/sqlite/migrations.go index 3a2f3179..f5374de7 100644 --- a/internal/storage/sqlite/migrations.go +++ b/internal/storage/sqlite/migrations.go @@ -53,6 +53,10 @@ var migrationsList = []Migration{ {"closed_by_session_column", migrations.MigrateClosedBySessionColumn}, {"due_defer_columns", migrations.MigrateDueDeferColumns}, {"owner_column", migrations.MigrateOwnerColumn}, + {"crystallizes_column", migrations.MigrateCrystallizesColumn}, + {"work_type_column", migrations.MigrateWorkTypeColumn}, + {"source_system_column", migrations.MigrateSourceSystemColumn}, + {"quality_score_column", migrations.MigrateQualityScoreColumn}, } // MigrationInfo contains metadata about a migration for inspection @@ -113,6 +117,10 @@ func getMigrationDescription(name string) string { "closed_by_session_column": "Adds closed_by_session column for tracking which Claude Code session closed an issue", "due_defer_columns": "Adds due_at and defer_until columns for time-based task scheduling (GH#820)", "owner_column": "Adds owner column for human attribution in HOP CV chains (Decision 008)", + "crystallizes_column": "Adds crystallizes column for work economics (compounds vs evaporates) per Decision 006", + "work_type_column": "Adds work_type column for work assignment model (mutex vs open_competition per Decision 006)", + "source_system_column": "Adds source_system column for federation adapter tracking", + "quality_score_column": "Adds quality_score column for aggregate quality (0.0-1.0) set by Refineries", } if desc, ok := descriptions[name]; ok { diff --git a/internal/storage/sqlite/migrations/037_crystallizes_column.go b/internal/storage/sqlite/migrations/037_crystallizes_column.go new file mode 100644 index 00000000..33652406 --- /dev/null +++ b/internal/storage/sqlite/migrations/037_crystallizes_column.go @@ -0,0 +1,36 @@ +package migrations + +import ( + "database/sql" + "fmt" +) + +// MigrateCrystallizesColumn adds the crystallizes column to the issues table. +// Crystallizes tracks whether work compounds over time (true: code, features) +// or evaporates (false: ops, support). Per Decision 006, this affects CV weighting. +// Default is false (conservative - work evaporates unless explicitly marked). +func MigrateCrystallizesColumn(db *sql.DB) error { + // Check if column already exists + var columnExists bool + err := db.QueryRow(` + SELECT COUNT(*) > 0 + FROM pragma_table_info('issues') + WHERE name = 'crystallizes' + `).Scan(&columnExists) + if err != nil { + return fmt.Errorf("failed to check crystallizes column: %w", err) + } + + if columnExists { + // Column already exists (e.g. created by new schema) + return nil + } + + // Add the crystallizes column + _, err = db.Exec(`ALTER TABLE issues ADD COLUMN crystallizes INTEGER DEFAULT 0`) + if err != nil { + return fmt.Errorf("failed to add crystallizes column: %w", err) + } + + return nil +} diff --git a/internal/storage/sqlite/migrations/038_work_type_column.go b/internal/storage/sqlite/migrations/038_work_type_column.go new file mode 100644 index 00000000..0027fbb2 --- /dev/null +++ b/internal/storage/sqlite/migrations/038_work_type_column.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "database/sql" + "fmt" +) + +// MigrateWorkTypeColumn adds work_type column to the issues table. +// This field distinguishes work assignment models per Decision 006. +// Values: 'mutex' (one worker, exclusive - default) or 'open_competition' (many submit, buyer picks) +func MigrateWorkTypeColumn(db *sql.DB) error { + // Check if column already exists + var columnExists bool + err := db.QueryRow(` + SELECT COUNT(*) > 0 + FROM pragma_table_info('issues') + WHERE name = 'work_type' + `).Scan(&columnExists) + if err != nil { + return fmt.Errorf("failed to check work_type column: %w", err) + } + + if columnExists { + return nil + } + + // Add the column with default 'mutex' + _, err = db.Exec(`ALTER TABLE issues ADD COLUMN work_type TEXT DEFAULT 'mutex'`) + if err != nil { + return fmt.Errorf("failed to add work_type column: %w", err) + } + + return nil +} diff --git a/internal/storage/sqlite/migrations/039_source_system_column.go b/internal/storage/sqlite/migrations/039_source_system_column.go new file mode 100644 index 00000000..b5b9e4de --- /dev/null +++ b/internal/storage/sqlite/migrations/039_source_system_column.go @@ -0,0 +1,33 @@ +package migrations + +import ( + "database/sql" + "fmt" +) + +// MigrateSourceSystemColumn adds the source_system column to the issues table. +// This tracks which adapter/system created the issue for federation support. +func MigrateSourceSystemColumn(db *sql.DB) error { + // Check if column already exists + var columnExists bool + err := db.QueryRow(` + SELECT COUNT(*) > 0 + FROM pragma_table_info('issues') + WHERE name = 'source_system' + `).Scan(&columnExists) + if err != nil { + return fmt.Errorf("failed to check source_system column: %w", err) + } + + if columnExists { + return nil + } + + // Add the source_system column + _, err = db.Exec(`ALTER TABLE issues ADD COLUMN source_system TEXT DEFAULT ''`) + if err != nil { + return fmt.Errorf("failed to add source_system column: %w", err) + } + + return nil +} diff --git a/internal/storage/sqlite/migrations/040_quality_score_column.go b/internal/storage/sqlite/migrations/040_quality_score_column.go new file mode 100644 index 00000000..f1be4503 --- /dev/null +++ b/internal/storage/sqlite/migrations/040_quality_score_column.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "database/sql" + "fmt" +) + +// MigrateQualityScoreColumn adds the quality_score column to the issues table. +// This stores an aggregate quality score (0.0-1.0) set by Refineries on merge. +// NULL indicates no score has been assigned yet. +func MigrateQualityScoreColumn(db *sql.DB) error { + // Check if column already exists + var columnExists bool + err := db.QueryRow(` + SELECT COUNT(*) > 0 + FROM pragma_table_info('issues') + WHERE name = 'quality_score' + `).Scan(&columnExists) + if err != nil { + return fmt.Errorf("failed to check quality_score column: %w", err) + } + + if columnExists { + return nil + } + + // Add the quality_score column (REAL, nullable - no default) + _, err = db.Exec(`ALTER TABLE issues ADD COLUMN quality_score REAL`) + if err != nil { + return fmt.Errorf("failed to add quality_score column: %w", err) + } + + return nil +} diff --git a/internal/storage/sqlite/schema.go b/internal/storage/sqlite/schema.go index 43d4d6fa..fdd24eef 100644 --- a/internal/storage/sqlite/schema.go +++ b/internal/storage/sqlite/schema.go @@ -39,6 +39,14 @@ CREATE TABLE IF NOT EXISTS issues ( is_template INTEGER DEFAULT 0, -- Molecule type field (bd-oxgi) mol_type TEXT DEFAULT '', + -- Work type field (Decision 006: mutex vs open_competition) + work_type TEXT DEFAULT 'mutex', + -- HOP quality score field (0.0-1.0, set by Refineries on merge) + quality_score REAL, + -- Work economics field (Decision 006) - compounds vs evaporates + crystallizes INTEGER DEFAULT 0, + -- Federation source system field + source_system TEXT DEFAULT '', -- Event fields (bd-ecmd) event_kind TEXT DEFAULT '', actor TEXT DEFAULT '', diff --git a/internal/types/types.go b/internal/types/types.go index 61a48d54..c418a3a9 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -46,7 +46,8 @@ type Issue struct { DeferUntil *time.Time `json:"defer_until,omitempty"` // Hide from bd ready until this time // ===== External Integration ===== - ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC" + ExternalRef *string `json:"external_ref,omitempty"` // e.g., "gh-9", "jira-ABC" + SourceSystem string `json:"source_system,omitempty"` // Adapter/system that created this issue (federation) // ===== Compaction Metadata ===== CompactionLevel int `json:"compaction_level,omitempty"` @@ -83,8 +84,10 @@ type Issue struct { BondedFrom []BondRef `json:"bonded_from,omitempty"` // For compounds: constituent protos // ===== HOP Fields (entity tracking for CV chains) ===== - Creator *EntityRef `json:"creator,omitempty"` // Who created (human, agent, or org) - Validations []Validation `json:"validations,omitempty"` // Who validated/approved + Creator *EntityRef `json:"creator,omitempty"` // Who created (human, agent, or org) + Validations []Validation `json:"validations,omitempty"` // Who validated/approved + QualityScore *float32 `json:"quality_score,omitempty"` // Aggregate quality (0.0-1.0), set by Refineries on merge + Crystallizes bool `json:"crystallizes,omitempty"` // Work that compounds (true: code, features) vs evaporates (false: ops, support) - affects CV weighting per Decision 006 // ===== Gate Fields (async coordination primitives) ===== AwaitType string `json:"await_type,omitempty"` // Condition type: gh:run, gh:pr, timer, human, mail @@ -110,6 +113,9 @@ type Issue struct { // ===== Molecule Type Fields (swarm coordination) ===== MolType MolType `json:"mol_type,omitempty"` // Molecule type: swarm|patrol|work (empty = work) + // ===== Work Type Fields (assignment model - Decision 006) ===== + WorkType WorkType `json:"work_type,omitempty"` // Work type: mutex|open_competition (empty = mutex) + // ===== Event Fields (operational state changes) ===== EventKind string `json:"event_kind,omitempty"` // Namespaced event type: patrol.muted, agent.started Actor string `json:"actor,omitempty"` // Entity URI who caused this event @@ -139,6 +145,7 @@ func (i *Issue) ComputeContentHash() string { // Optional fields w.strPtr(i.ExternalRef) + w.str(i.SourceSystem) w.flag(i.Pinned, "pinned") w.flag(i.IsTemplate, "template") @@ -160,6 +167,10 @@ func (i *Issue) ComputeContentHash() string { w.float32Ptr(v.Score) } + // HOP aggregate quality score and crystallizes + w.float32Ptr(i.QualityScore) + w.flag(i.Crystallizes, "crystallizes") + // Gate fields for async coordination w.str(i.AwaitType) w.str(i.AwaitID) @@ -181,6 +192,9 @@ func (i *Issue) ComputeContentHash() string { // Molecule type w.str(string(i.MolType)) + // Work type + w.str(string(i.WorkType)) + // Event fields w.str(i.EventKind) w.str(i.Actor) @@ -587,6 +601,24 @@ func (m MolType) IsValid() bool { return false } +// WorkType categorizes how work assignment operates for a bead (Decision 006) +type WorkType string + +// WorkType constants +const ( + WorkTypeMutex WorkType = "mutex" // One worker, exclusive assignment (default) + WorkTypeOpenCompetition WorkType = "open_competition" // Many submit, buyer picks +) + +// IsValid checks if the work type value is valid +func (w WorkType) IsValid() bool { + switch w { + case WorkTypeMutex, WorkTypeOpenCompetition, "": + return true // empty is valid (defaults to mutex) + } + return false +} + // Dependency represents a relationship between issues type Dependency struct { IssueID string `json:"issue_id"` @@ -667,6 +699,9 @@ const ( DepUntil DependencyType = "until" // Active until target closes (e.g., muted until issue resolved) DepCausedBy DependencyType = "caused-by" // Triggered by target (audit trail) DepValidates DependencyType = "validates" // Approval/validation relationship + + // Delegation types (work delegation chains) + DepDelegatedFrom DependencyType = "delegated-from" // Work delegated from parent; completion cascades up ) // IsValid checks if the dependency type value is valid. @@ -683,7 +718,7 @@ func (d DependencyType) IsWellKnown() bool { case DepBlocks, DepParentChild, DepConditionalBlocks, DepWaitsFor, DepRelated, DepDiscoveredFrom, DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes, DepAuthoredBy, DepAssignedTo, DepApprovedBy, DepAttests, DepTracks, - DepUntil, DepCausedBy, DepValidates: + DepUntil, DepCausedBy, DepValidates, DepDelegatedFrom: return true } return false diff --git a/internal/types/types_test.go b/internal/types/types_test.go index f17536c3..8db642ed 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -550,7 +550,6 @@ func TestIssueTypeIsValid(t *testing.T) { {TypeGate, true}, {TypeAgent, true}, {TypeRole, true}, - {TypeRig, true}, {TypeConvoy, true}, {TypeEvent, true}, {TypeSlot, true}, @@ -567,46 +566,6 @@ func TestIssueTypeIsValid(t *testing.T) { } } -func TestIssueTypeIsBuiltIn(t *testing.T) { - // IsBuiltIn should match IsValid - both identify built-in types - // This is used during multi-repo hydration to determine trust: - // - Built-in types: validate (catch typos) - // - Custom types (!IsBuiltIn): trust from source repo - tests := []struct { - issueType IssueType - builtIn bool - }{ - // All built-in types - {TypeBug, true}, - {TypeFeature, true}, - {TypeTask, true}, - {TypeEpic, true}, - {TypeChore, true}, - {TypeMessage, true}, - {TypeMergeRequest, true}, - {TypeMolecule, true}, - {TypeGate, true}, - {TypeAgent, true}, - {TypeRole, true}, - {TypeRig, true}, - {TypeConvoy, true}, - {TypeEvent, true}, - {TypeSlot, true}, - // Custom types (not built-in) - {IssueType("pm"), false}, // Custom type from child repo - {IssueType("llm"), false}, // Custom type from child repo - {IssueType(""), false}, // Empty is not built-in - } - - for _, tt := range tests { - t.Run(string(tt.issueType), func(t *testing.T) { - if got := tt.issueType.IsBuiltIn(); got != tt.builtIn { - t.Errorf("IssueType(%q).IsBuiltIn() = %v, want %v", tt.issueType, got, tt.builtIn) - } - }) - } -} - func TestIssueTypeRequiredSections(t *testing.T) { tests := []struct { issueType IssueType