feat(types): add template molecules infrastructure for beads-1ra
Add support for template molecules (is_template field and TypeMolecule type): - Add IsTemplate field to Issue type with JSON support - Add TypeMolecule constant to IssueType constants - Add IsTemplate filter to IssueFilter for querying - Update all SQL queries to include is_template column - Add migration 024 for is_template column - Add FindMoleculesJSONLInDir helper for molecules.jsonl path detection This enables treating certain issues as read-only templates that can be instantiated to create work items. The template flag allows separating template molecules from regular work items in queries and exports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,11 +211,12 @@ const (
|
|||||||
|
|
||||||
// IssueType constants
|
// IssueType constants
|
||||||
const (
|
const (
|
||||||
TypeBug = types.TypeBug
|
TypeBug = types.TypeBug
|
||||||
TypeFeature = types.TypeFeature
|
TypeFeature = types.TypeFeature
|
||||||
TypeTask = types.TypeTask
|
TypeTask = types.TypeTask
|
||||||
TypeEpic = types.TypeEpic
|
TypeEpic = types.TypeEpic
|
||||||
TypeChore = types.TypeChore
|
TypeChore = types.TypeChore
|
||||||
|
TypeMolecule = types.TypeMolecule
|
||||||
)
|
)
|
||||||
|
|
||||||
// DependencyType constants
|
// DependencyType constants
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID
|
|||||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
i.created_at, 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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||||
i.sender, i.ephemeral, i.pinned,
|
i.sender, i.ephemeral, i.pinned, i.is_template,
|
||||||
d.type
|
d.type
|
||||||
FROM issues i
|
FROM issues i
|
||||||
JOIN dependencies d ON i.id = d.depends_on_id
|
JOIN dependencies d ON i.id = d.depends_on_id
|
||||||
@@ -255,7 +255,7 @@ func (s *SQLiteStorage) GetDependentsWithMetadata(ctx context.Context, issueID s
|
|||||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
i.created_at, 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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||||
i.sender, i.ephemeral, i.pinned,
|
i.sender, i.ephemeral, i.pinned, i.is_template,
|
||||||
d.type
|
d.type
|
||||||
FROM issues i
|
FROM issues i
|
||||||
JOIN dependencies d ON i.id = d.issue_id
|
JOIN dependencies d ON i.id = d.issue_id
|
||||||
@@ -716,6 +716,8 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
@@ -723,7 +725,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
|||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
||||||
@@ -772,6 +774,10 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
issues = append(issues, &issue)
|
issues = append(issues, &issue)
|
||||||
issueIDs = append(issueIDs, issue.ID)
|
issueIDs = append(issueIDs, issue.ID)
|
||||||
@@ -813,6 +819,8 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
var depType types.DependencyType
|
var depType types.DependencyType
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
@@ -821,7 +829,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
|||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
&depType,
|
&depType,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -868,6 +876,10 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch labels for this issue
|
// Fetch labels for this issue
|
||||||
labels, err := s.GetLabels(ctx, issue.ID)
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
|||||||
if issue.Pinned {
|
if issue.Pinned {
|
||||||
pinned = 1
|
pinned = 1
|
||||||
}
|
}
|
||||||
|
isTemplate := 0
|
||||||
|
if issue.IsTemplate {
|
||||||
|
isTemplate = 1
|
||||||
|
}
|
||||||
|
|
||||||
_, err := conn.ExecContext(ctx, `
|
_, err := conn.ExecContext(ctx, `
|
||||||
INSERT OR IGNORE INTO issues (
|
INSERT OR IGNORE INTO issues (
|
||||||
@@ -42,8 +46,8 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
|||||||
status, priority, issue_type, assignee, estimated_minutes,
|
status, priority, issue_type, assignee, estimated_minutes,
|
||||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
||||||
@@ -51,7 +55,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
|||||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||||
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
||||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, ephemeral, pinned,
|
issue.Sender, ephemeral, pinned, isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
||||||
@@ -72,8 +76,8 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
|||||||
status, priority, issue_type, assignee, estimated_minutes,
|
status, priority, issue_type, assignee, estimated_minutes,
|
||||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||||
@@ -94,6 +98,10 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
|||||||
if issue.Pinned {
|
if issue.Pinned {
|
||||||
pinned = 1
|
pinned = 1
|
||||||
}
|
}
|
||||||
|
isTemplate := 0
|
||||||
|
if issue.IsTemplate {
|
||||||
|
isTemplate = 1
|
||||||
|
}
|
||||||
|
|
||||||
_, err = stmt.ExecContext(ctx,
|
_, err = stmt.ExecContext(ctx,
|
||||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||||
@@ -102,7 +110,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
|||||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||||
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
||||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, ephemeral, pinned,
|
issue.Sender, ephemeral, pinned, isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*
|
|||||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
i.created_at, 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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||||
i.sender, i.ephemeral, i.pinned
|
i.sender, i.ephemeral, i.pinned, i.is_template
|
||||||
FROM issues i
|
FROM issues i
|
||||||
JOIN labels l ON i.id = l.issue_id
|
JOIN labels l ON i.id = l.issue_id
|
||||||
WHERE l.label = ?
|
WHERE l.label = ?
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ var migrationsList = []Migration{
|
|||||||
{"migrate_edge_fields", migrations.MigrateEdgeFields},
|
{"migrate_edge_fields", migrations.MigrateEdgeFields},
|
||||||
{"drop_edge_columns", migrations.MigrateDropEdgeColumns},
|
{"drop_edge_columns", migrations.MigrateDropEdgeColumns},
|
||||||
{"pinned_column", migrations.MigratePinnedColumn},
|
{"pinned_column", migrations.MigratePinnedColumn},
|
||||||
|
{"is_template_column", migrations.MigrateIsTemplateColumn},
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrationInfo contains metadata about a migration for inspection
|
// MigrationInfo contains metadata about a migration for inspection
|
||||||
@@ -87,6 +88,7 @@ func getMigrationDescription(name string) string {
|
|||||||
"migrate_edge_fields": "Migrates existing issue fields (replies_to, relates_to, duplicate_of, superseded_by) to dependency edges (Decision 004 Phase 3)",
|
"migrate_edge_fields": "Migrates existing issue fields (replies_to, relates_to, duplicate_of, superseded_by) to dependency edges (Decision 004 Phase 3)",
|
||||||
"drop_edge_columns": "Drops deprecated edge columns (replies_to, relates_to, duplicate_of, superseded_by) from issues table (Decision 004 Phase 4)",
|
"drop_edge_columns": "Drops deprecated edge columns (replies_to, relates_to, duplicate_of, superseded_by) from issues table (Decision 004 Phase 4)",
|
||||||
"pinned_column": "Adds pinned column for persistent context markers (bd-7h5)",
|
"pinned_column": "Adds pinned column for persistent context markers (bd-7h5)",
|
||||||
|
"is_template_column": "Adds is_template column for template molecules (beads-1ra)",
|
||||||
}
|
}
|
||||||
|
|
||||||
if desc, ok := descriptions[name]; ok {
|
if desc, ok := descriptions[name]; ok {
|
||||||
|
|||||||
40
internal/storage/sqlite/migrations/024_is_template_column.go
Normal file
40
internal/storage/sqlite/migrations/024_is_template_column.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrateIsTemplateColumn adds the is_template column to the issues table.
|
||||||
|
// Template issues (molecules) are read-only templates that should be filtered
|
||||||
|
// from work views by default (beads-1ra).
|
||||||
|
func MigrateIsTemplateColumn(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 = 'is_template'
|
||||||
|
`).Scan(&columnExists)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check is_template column: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the is_template column
|
||||||
|
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN is_template INTEGER DEFAULT 0`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add is_template column: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add index for template issues (for efficient filtering)
|
||||||
|
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_issues_is_template ON issues(is_template) WHERE is_template = 1`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create is_template index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -486,13 +486,14 @@ func TestMigrateContentHashColumn(t *testing.T) {
|
|||||||
sender TEXT DEFAULT '',
|
sender TEXT DEFAULT '',
|
||||||
ephemeral INTEGER DEFAULT 0,
|
ephemeral INTEGER DEFAULT 0,
|
||||||
pinned INTEGER DEFAULT 0,
|
pinned INTEGER DEFAULT 0,
|
||||||
|
is_template INTEGER DEFAULT 0,
|
||||||
replies_to TEXT DEFAULT '',
|
replies_to TEXT DEFAULT '',
|
||||||
relates_to TEXT DEFAULT '',
|
relates_to TEXT DEFAULT '',
|
||||||
duplicate_of TEXT DEFAULT '',
|
duplicate_of TEXT DEFAULT '',
|
||||||
superseded_by TEXT DEFAULT '',
|
superseded_by TEXT DEFAULT '',
|
||||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
||||||
);
|
);
|
||||||
INSERT INTO issues SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, created_at, updated_at, closed_at, external_ref, compaction_level, compacted_at, original_size, compacted_at_commit, source_repo, '', NULL, '', '', '', '', 0, 0, '', '', '', '' FROM issues_backup;
|
INSERT INTO issues SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee, estimated_minutes, created_at, updated_at, closed_at, external_ref, compaction_level, compacted_at, original_size, compacted_at_commit, source_repo, '', NULL, '', '', '', '', 0, 0, 0, '', '', '', '' FROM issues_backup;
|
||||||
DROP TABLE issues_backup;
|
DROP TABLE issues_backup;
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
|||||||
if issue.Pinned {
|
if issue.Pinned {
|
||||||
pinned = 1
|
pinned = 1
|
||||||
}
|
}
|
||||||
|
isTemplate := 0
|
||||||
|
if issue.IsTemplate {
|
||||||
|
isTemplate = 1
|
||||||
|
}
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// Issue doesn't exist - insert it
|
// Issue doesn't exist - insert it
|
||||||
@@ -274,8 +278,8 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
|||||||
status, priority, issue_type, assignee, estimated_minutes,
|
status, priority, issue_type, assignee, estimated_minutes,
|
||||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
||||||
@@ -283,7 +287,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
|||||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||||
issue.ClosedAt, issue.ExternalRef, issue.SourceRepo, issue.CloseReason,
|
issue.ClosedAt, issue.ExternalRef, issue.SourceRepo, issue.CloseReason,
|
||||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, ephemeral, pinned,
|
issue.Sender, ephemeral, pinned, isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to insert issue: %w", err)
|
return fmt.Errorf("failed to insert issue: %w", err)
|
||||||
@@ -307,7 +311,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
|||||||
issue_type = ?, assignee = ?, estimated_minutes = ?,
|
issue_type = ?, assignee = ?, estimated_minutes = ?,
|
||||||
updated_at = ?, closed_at = ?, external_ref = ?, source_repo = ?,
|
updated_at = ?, closed_at = ?, external_ref = ?, source_repo = ?,
|
||||||
deleted_at = ?, deleted_by = ?, delete_reason = ?, original_type = ?,
|
deleted_at = ?, deleted_by = ?, delete_reason = ?, original_type = ?,
|
||||||
sender = ?, ephemeral = ?, pinned = ?
|
sender = ?, ephemeral = ?, pinned = ?, is_template = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||||
@@ -315,7 +319,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
|||||||
issue.IssueType, issue.Assignee, issue.EstimatedMinutes,
|
issue.IssueType, issue.Assignee, issue.EstimatedMinutes,
|
||||||
issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, issue.SourceRepo,
|
issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, issue.SourceRepo,
|
||||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||||
issue.Sender, ephemeral, pinned,
|
issue.Sender, ephemeral, pinned, isTemplate,
|
||||||
issue.ID,
|
issue.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -250,6 +250,8 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
|
|
||||||
var contentHash sql.NullString
|
var contentHash sql.NullString
|
||||||
var compactedAtCommit sql.NullString
|
var compactedAtCommit sql.NullString
|
||||||
@@ -259,7 +261,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
created_at, updated_at, closed_at, external_ref,
|
created_at, updated_at, closed_at, external_ref,
|
||||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, id).Scan(
|
`, id).Scan(
|
||||||
@@ -269,7 +271,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -331,6 +333,10 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch labels for this issue
|
// Fetch labels for this issue
|
||||||
labels, err := s.GetLabels(ctx, issue.ID)
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
@@ -439,6 +445,8 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
|
|
||||||
err := s.db.QueryRowContext(ctx, `
|
err := s.db.QueryRowContext(ctx, `
|
||||||
SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
|
SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
|
||||||
@@ -446,7 +454,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
created_at, updated_at, closed_at, external_ref,
|
created_at, updated_at, closed_at, external_ref,
|
||||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
WHERE external_ref = ?
|
WHERE external_ref = ?
|
||||||
`, externalRef).Scan(
|
`, externalRef).Scan(
|
||||||
@@ -456,7 +464,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRefCol,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRefCol,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -518,6 +526,10 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch labels for this issue
|
// Fetch labels for this issue
|
||||||
labels, err := s.GetLabels(ctx, issue.ID)
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
@@ -1589,6 +1601,15 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template filtering (beads-1ra)
|
||||||
|
if filter.IsTemplate != nil {
|
||||||
|
if *filter.IsTemplate {
|
||||||
|
whereClauses = append(whereClauses, "is_template = 1")
|
||||||
|
} else {
|
||||||
|
whereClauses = append(whereClauses, "(is_template = 0 OR is_template IS NULL)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
whereSQL := ""
|
whereSQL := ""
|
||||||
if len(whereClauses) > 0 {
|
if len(whereClauses) > 0 {
|
||||||
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
||||||
@@ -1606,7 +1627,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
|||||||
status, priority, issue_type, assignee, estimated_minutes,
|
status, priority, issue_type, assignee, estimated_minutes,
|
||||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
%s
|
%s
|
||||||
ORDER BY priority ASC, created_at DESC
|
ORDER BY priority ASC, created_at DESC
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
|||||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
i.created_at, 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.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||||
i.sender, i.ephemeral, i.pinned
|
i.sender, i.ephemeral, i.pinned, i.is_template
|
||||||
FROM issues i
|
FROM issues i
|
||||||
WHERE %s
|
WHERE %s
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
@@ -139,7 +139,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
created_at, updated_at, closed_at, external_ref, source_repo,
|
created_at, updated_at, closed_at, external_ref, source_repo,
|
||||||
compaction_level, compacted_at, compacted_at_commit, original_size, close_reason,
|
compaction_level, compacted_at, compacted_at_commit, original_size, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
WHERE status != 'closed'
|
WHERE status != 'closed'
|
||||||
AND datetime(updated_at) < datetime('now', '-' || ? || ' days')
|
AND datetime(updated_at) < datetime('now', '-' || ? || ' days')
|
||||||
@@ -190,6 +190,8 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
@@ -198,7 +200,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
||||||
&compactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &closeReason,
|
&compactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan stale issue: %w", err)
|
return nil, fmt.Errorf("failed to scan stale issue: %w", err)
|
||||||
@@ -259,6 +261,10 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
issues = append(issues, &issue)
|
issues = append(issues, &issue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
|||||||
ephemeral INTEGER DEFAULT 0,
|
ephemeral INTEGER DEFAULT 0,
|
||||||
-- Pinned field (bd-7h5)
|
-- Pinned field (bd-7h5)
|
||||||
pinned INTEGER DEFAULT 0,
|
pinned INTEGER DEFAULT 0,
|
||||||
|
-- Template field (beads-1ra)
|
||||||
|
is_template INTEGER DEFAULT 0,
|
||||||
-- NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
-- NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||||
-- These relationships are now stored in the dependencies table
|
-- These relationships are now stored in the dependencies table
|
||||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue
|
|||||||
created_at, updated_at, closed_at, external_ref,
|
created_at, updated_at, closed_at, external_ref,
|
||||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, id)
|
`, id)
|
||||||
@@ -1107,7 +1107,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
|
|||||||
created_at, updated_at, closed_at, external_ref,
|
created_at, updated_at, closed_at, external_ref,
|
||||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||||
deleted_at, deleted_by, delete_reason, original_type,
|
deleted_at, deleted_by, delete_reason, original_type,
|
||||||
sender, ephemeral, pinned
|
sender, ephemeral, pinned, is_template
|
||||||
FROM issues
|
FROM issues
|
||||||
%s
|
%s
|
||||||
ORDER BY priority ASC, created_at DESC
|
ORDER BY priority ASC, created_at DESC
|
||||||
@@ -1152,6 +1152,8 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
|||||||
var ephemeral sql.NullInt64
|
var ephemeral sql.NullInt64
|
||||||
// Pinned field (bd-7h5)
|
// Pinned field (bd-7h5)
|
||||||
var pinned sql.NullInt64
|
var pinned sql.NullInt64
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
var isTemplate sql.NullInt64
|
||||||
|
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
@@ -1160,7 +1162,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
|||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
||||||
@@ -1218,6 +1220,10 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
|||||||
if pinned.Valid && pinned.Int64 != 0 {
|
if pinned.Valid && pinned.Int64 != 0 {
|
||||||
issue.Pinned = true
|
issue.Pinned = true
|
||||||
}
|
}
|
||||||
|
// Template field (beads-1ra)
|
||||||
|
if isTemplate.Valid && isTemplate.Int64 != 0 {
|
||||||
|
issue.IsTemplate = true
|
||||||
|
}
|
||||||
|
|
||||||
return &issue, nil
|
return &issue, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ type Issue struct {
|
|||||||
|
|
||||||
// Pinned field (bd-7h5): persistent context markers
|
// Pinned field (bd-7h5): persistent context markers
|
||||||
Pinned bool `json:"pinned,omitempty"` // If true, issue is a persistent context marker, not a work item
|
Pinned bool `json:"pinned,omitempty"` // If true, issue is a persistent context marker, not a work item
|
||||||
|
|
||||||
|
// Template field (beads-1ra): template molecule support
|
||||||
|
IsTemplate bool `json:"is_template,omitempty"` // If true, issue is a read-only template molecule
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputeContentHash creates a deterministic hash of the issue's content.
|
// ComputeContentHash creates a deterministic hash of the issue's content.
|
||||||
@@ -83,6 +86,10 @@ func (i *Issue) ComputeContentHash() string {
|
|||||||
if i.Pinned {
|
if i.Pinned {
|
||||||
h.Write([]byte("pinned"))
|
h.Write([]byte("pinned"))
|
||||||
}
|
}
|
||||||
|
h.Write([]byte{0})
|
||||||
|
if i.IsTemplate {
|
||||||
|
h.Write([]byte("template"))
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
@@ -233,7 +240,7 @@ const (
|
|||||||
TypeChore IssueType = "chore"
|
TypeChore IssueType = "chore"
|
||||||
TypeMessage IssueType = "message" // Ephemeral communication between workers
|
TypeMessage IssueType = "message" // Ephemeral communication between workers
|
||||||
TypeMergeRequest IssueType = "merge-request" // Merge queue entry for refinery processing
|
TypeMergeRequest IssueType = "merge-request" // Merge queue entry for refinery processing
|
||||||
TypeMolecule IssueType = "molecule" // Composable workflow template
|
TypeMolecule IssueType = "molecule" // Template molecule for issue hierarchies (beads-1ra)
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValid checks if the issue type value is valid
|
// IsValid checks if the issue type value is valid
|
||||||
@@ -446,6 +453,9 @@ type IssueFilter struct {
|
|||||||
|
|
||||||
// Pinned filtering (bd-7h5)
|
// Pinned filtering (bd-7h5)
|
||||||
Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned)
|
Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned)
|
||||||
|
|
||||||
|
// Template filtering (beads-1ra)
|
||||||
|
IsTemplate *bool // Filter by template flag (nil = any, true = only templates, false = exclude templates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortPolicy determines how ready work is ordered
|
// SortPolicy determines how ready work is ordered
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +56,19 @@ func FindJSONLInDir(dbDir string) string {
|
|||||||
return filepath.Join(dbDir, "issues.jsonl")
|
return filepath.Join(dbDir, "issues.jsonl")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindMoleculesJSONLInDir finds the molecules.jsonl file in the given .beads directory.
|
||||||
|
// Returns the path to molecules.jsonl if it exists, empty string otherwise.
|
||||||
|
// Molecules are template issues used for instantiation (beads-1ra).
|
||||||
|
func FindMoleculesJSONLInDir(dbDir string) string {
|
||||||
|
moleculesPath := filepath.Join(dbDir, "molecules.jsonl")
|
||||||
|
// Check if file exists - we don't fall back to any other file
|
||||||
|
// because molecules.jsonl is optional and specific
|
||||||
|
if _, err := os.Stat(moleculesPath); err == nil {
|
||||||
|
return moleculesPath
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// CanonicalizePath converts a path to its canonical form by:
|
// CanonicalizePath converts a path to its canonical form by:
|
||||||
// 1. Converting to absolute path
|
// 1. Converting to absolute path
|
||||||
// 2. Resolving symlinks
|
// 2. Resolving symlinks
|
||||||
|
|||||||
Reference in New Issue
Block a user