feat: add crystallizes column to sqlite storage

Adds crystallizes column for work economics (compounds vs evaporates)
per Decision 006. Includes migration 036 and updates to all INSERT/SELECT
queries in issues.go, queries.go, dependencies.go, and transaction.go.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/fang
2026-01-10 23:42:58 -08:00
committed by Steve Yegge
parent 0ed349b3ed
commit f5cd36752d
6 changed files with 97 additions and 17 deletions

View File

@@ -249,7 +249,7 @@ func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template,
i.sender, i.ephemeral, i.pinned, i.is_template, i.crystallizes,
i.await_type, i.await_id, i.timeout_ns, i.waiters,
d.type
FROM issues i
@@ -272,7 +272,7 @@ func (s *SQLiteStorage) GetDependentsWithMetadata(ctx context.Context, issueID s
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
i.created_at, i.created_by, i.owner, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
i.sender, i.ephemeral, i.pinned, i.is_template,
i.sender, i.ephemeral, i.pinned, i.is_template, i.crystallizes,
i.await_type, i.await_id, i.timeout_ns, i.waiters,
d.type
FROM issues i
@@ -879,6 +879,8 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
var pinned sql.NullInt64
// Template field
var isTemplate sql.NullInt64
// Crystallizes field (work economics)
var crystallizes sql.NullInt64
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
@@ -891,7 +893,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
)
if err != nil {
@@ -948,6 +950,10 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Crystallizes field (work economics)
if crystallizes.Valid && crystallizes.Int64 != 0 {
issue.Crystallizes = true
}
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
@@ -1005,6 +1011,8 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
var pinned sql.NullInt64
// Template field
var isTemplate sql.NullInt64
// Crystallizes field (work economics)
var crystallizes sql.NullInt64
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
@@ -1018,7 +1026,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
&depType,
)
@@ -1073,6 +1081,10 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Crystallizes field (work economics)
if crystallizes.Valid && crystallizes.Int64 != 0 {
issue.Crystallizes = true
}
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String

View File

@@ -41,6 +41,10 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
if issue.IsTemplate {
isTemplate = 1
}
crystallizes := 0
if issue.Crystallizes {
crystallizes = 1
}
_, err := conn.ExecContext(ctx, `
INSERT OR IGNORE INTO issues (
@@ -48,7 +52,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
status, priority, issue_type, assignee, estimated_minutes,
created_at, created_by, owner, updated_at, closed_at, external_ref, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload,
due_at, defer_until
@@ -60,7 +64,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt,
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
issue.Sender, wisp, pinned, isTemplate,
issue.Sender, wisp, pinned, isTemplate, crystallizes,
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
string(issue.MolType),
issue.EventKind, issue.Actor, issue.Target, issue.Payload,
@@ -99,6 +103,10 @@ func insertIssueStrict(ctx context.Context, conn *sql.Conn, issue *types.Issue)
if issue.IsTemplate {
isTemplate = 1
}
crystallizes := 0
if issue.Crystallizes {
crystallizes = 1
}
_, err := conn.ExecContext(ctx, `
INSERT INTO issues (
@@ -106,7 +114,7 @@ func insertIssueStrict(ctx context.Context, conn *sql.Conn, issue *types.Issue)
status, priority, issue_type, assignee, estimated_minutes,
created_at, created_by, owner, updated_at, closed_at, external_ref, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload,
due_at, defer_until
@@ -118,7 +126,7 @@ func insertIssueStrict(ctx context.Context, conn *sql.Conn, issue *types.Issue)
issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt,
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
issue.Sender, wisp, pinned, isTemplate,
issue.Sender, wisp, pinned, isTemplate, crystallizes,
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
string(issue.MolType),
issue.EventKind, issue.Actor, issue.Target, issue.Payload,
@@ -138,7 +146,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
status, priority, issue_type, assignee, estimated_minutes,
created_at, created_by, owner, updated_at, closed_at, external_ref, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters, mol_type,
event_kind, actor, target, payload,
due_at, defer_until
@@ -167,6 +175,10 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
if issue.IsTemplate {
isTemplate = 1
}
crystallizes := 0
if issue.Crystallizes {
crystallizes = 1
}
_, err = stmt.ExecContext(ctx,
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
@@ -175,7 +187,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.Owner, issue.UpdatedAt,
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
issue.Sender, wisp, pinned, isTemplate,
issue.Sender, wisp, pinned, isTemplate, crystallizes,
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
string(issue.MolType),
issue.EventKind, issue.Actor, issue.Target, issue.Payload,

View File

@@ -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
}

View File

@@ -273,6 +273,8 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
var pinned sql.NullInt64
// Template field
var isTemplate sql.NullInt64
// Crystallizes field (work economics)
var crystallizes sql.NullInt64
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
@@ -305,7 +307,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
created_at, created_by, owner, updated_at, closed_at, external_ref,
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters,
hook_bead, role_bead, agent_state, last_activity, role_type, rig, mol_type,
event_kind, actor, target, payload,
@@ -319,7 +321,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef,
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
&hookBead, &roleBead, &agentState, &lastActivity, &roleType, &rig, &molType,
&eventKind, &actor, &target, &payload,
@@ -392,6 +394,10 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Crystallizes field (work economics)
if crystallizes.Valid && crystallizes.Int64 != 0 {
issue.Crystallizes = true
}
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
@@ -558,6 +564,8 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
var pinned sql.NullInt64
// Template field
var isTemplate sql.NullInt64
// Crystallizes field (work economics)
var crystallizes sql.NullInt64
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
@@ -571,7 +579,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
created_at, created_by, owner, updated_at, closed_at, external_ref,
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters
FROM issues
WHERE external_ref = ?
@@ -582,7 +590,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRefCol,
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
)
@@ -652,6 +660,10 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Crystallizes field (work economics)
if crystallizes.Valid && crystallizes.Int64 != 0 {
issue.Crystallizes = true
}
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
@@ -1938,7 +1950,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
status, priority, issue_type, assignee, estimated_minutes,
created_at, created_by, owner, updated_at, closed_at, external_ref, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters
FROM issues
%s

View File

@@ -37,6 +37,8 @@ CREATE TABLE IF NOT EXISTS issues (
pinned INTEGER DEFAULT 0,
-- Template field (beads-1ra)
is_template INTEGER DEFAULT 0,
-- Work economics field (bd-fqze8) - HOP Decision 006
crystallizes INTEGER DEFAULT 0,
-- Molecule type field (bd-oxgi)
mol_type TEXT DEFAULT '',
-- Work type field (Decision 006: mutex vs open_competition)

View File

@@ -330,7 +330,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue
created_at, created_by, owner, updated_at, closed_at, external_ref,
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
deleted_at, deleted_by, delete_reason, original_type,
sender, ephemeral, pinned, is_template,
sender, ephemeral, pinned, is_template, crystallizes,
await_type, await_id, timeout_ns, waiters
FROM issues
WHERE id = ?
@@ -1308,6 +1308,8 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
var pinned sql.NullInt64
// Template field
var isTemplate sql.NullInt64
// Crystallizes field (work economics)
var crystallizes sql.NullInt64
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
@@ -1321,7 +1323,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef,
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
)
if err != nil {
@@ -1387,6 +1389,10 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Crystallizes field (work economics)
if crystallizes.Valid && crystallizes.Int64 != 0 {
issue.Crystallizes = true
}
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String