Phase 1: Edge schema consolidation infrastructure (Decision 004)

Add metadata and thread_id columns to dependencies table to support:
- Edge metadata: JSON blob for type-specific data (similarity scores, etc.)
- Thread queries: O(1) conversation threading via thread_id

Changes:
- New migration 020_edge_consolidation.go
- Updated Dependency struct with Metadata and ThreadID fields
- Added new entity types: authored-by, assigned-to, approved-by
- Relaxed DependencyType validation (any non-empty string ≤50 chars)
- Added IsWellKnown() and AffectsReadyWork() methods
- Updated SQL queries to include new columns
- Updated tests for new behavior

This enables HOP knowledge graph requirements and Reddit-style threading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-18 01:17:57 -08:00
parent b4a6ee4f5f
commit d390aa8834
6 changed files with 191 additions and 22 deletions

View File

@@ -244,6 +244,12 @@ type Dependency struct {
Type DependencyType `json:"type"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
// Metadata contains type-specific edge data (JSON blob)
// Examples: similarity scores, approval details, skill proficiency
Metadata string `json:"metadata,omitempty"`
// ThreadID groups conversation edges for efficient thread queries
// For replies-to edges, this identifies the conversation root
ThreadID string `json:"thread_id,omitempty"`
}
// DependencyCounts holds counts for dependencies and dependents
@@ -271,27 +277,51 @@ type DependencyType string
// Dependency type constants
const (
DepBlocks DependencyType = "blocks"
// Workflow types (affect ready work calculation)
DepBlocks DependencyType = "blocks"
DepParentChild DependencyType = "parent-child"
// Association types
DepRelated DependencyType = "related"
DepParentChild DependencyType = "parent-child"
DepDiscoveredFrom DependencyType = "discovered-from"
// Graph link types (bd-kwro)
DepRepliesTo DependencyType = "replies-to" // Conversation threading
DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges
DepDuplicates DependencyType = "duplicates" // Deduplication link
DepSupersedes DependencyType = "supersedes" // Version chain link
DepRepliesTo DependencyType = "replies-to" // Conversation threading
DepRelatesTo DependencyType = "relates-to" // Loose knowledge graph edges
DepDuplicates DependencyType = "duplicates" // Deduplication link
DepSupersedes DependencyType = "supersedes" // Version chain link
// Entity types (HOP foundation - Decision 004)
DepAuthoredBy DependencyType = "authored-by" // Creator relationship
DepAssignedTo DependencyType = "assigned-to" // Assignment relationship
DepApprovedBy DependencyType = "approved-by" // Approval relationship
)
// IsValid checks if the dependency type value is valid
// IsValid checks if the dependency type value is valid.
// Accepts any non-empty string up to 50 characters.
// Use IsWellKnown() to check if it's a built-in type.
func (d DependencyType) IsValid() bool {
return len(d) > 0 && len(d) <= 50
}
// IsWellKnown checks if the dependency type is a well-known constant.
// Returns false for custom/user-defined types (which are still valid).
func (d DependencyType) IsWellKnown() bool {
switch d {
case DepBlocks, DepRelated, DepParentChild, DepDiscoveredFrom,
DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes:
case DepBlocks, DepParentChild, DepRelated, DepDiscoveredFrom,
DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes,
DepAuthoredBy, DepAssignedTo, DepApprovedBy:
return true
}
return false
}
// AffectsReadyWork returns true if this dependency type blocks work.
// Only "blocks" and "parent-child" relationships affect the ready work calculation.
func (d DependencyType) AffectsReadyWork() bool {
return d == DepBlocks || d == DepParentChild
}
// Label represents a tag on an issue
type Label struct {
IssueID string `json:"issue_id"`

View File

@@ -415,6 +415,7 @@ func TestIssueTypeIsValid(t *testing.T) {
}
func TestDependencyTypeIsValid(t *testing.T) {
// IsValid now accepts any non-empty string up to 50 chars (Decision 004)
tests := []struct {
depType DependencyType
valid bool
@@ -423,8 +424,17 @@ func TestDependencyTypeIsValid(t *testing.T) {
{DepRelated, true},
{DepParentChild, true},
{DepDiscoveredFrom, true},
{DependencyType("invalid"), false},
{DependencyType(""), false},
{DepRepliesTo, true},
{DepRelatesTo, true},
{DepDuplicates, true},
{DepSupersedes, true},
{DepAuthoredBy, true},
{DepAssignedTo, true},
{DepApprovedBy, true},
{DependencyType("custom-type"), true}, // Custom types are now valid
{DependencyType("any-string"), true}, // Any non-empty string is valid
{DependencyType(""), false}, // Empty is still invalid
{DependencyType("this-is-a-very-long-dependency-type-that-exceeds-fifty-characters"), false}, // Too long
}
for _, tt := range tests {
@@ -436,6 +446,63 @@ func TestDependencyTypeIsValid(t *testing.T) {
}
}
func TestDependencyTypeIsWellKnown(t *testing.T) {
tests := []struct {
depType DependencyType
wellKnown bool
}{
{DepBlocks, true},
{DepRelated, true},
{DepParentChild, true},
{DepDiscoveredFrom, true},
{DepRepliesTo, true},
{DepRelatesTo, true},
{DepDuplicates, true},
{DepSupersedes, true},
{DepAuthoredBy, true},
{DepAssignedTo, true},
{DepApprovedBy, true},
{DependencyType("custom-type"), false},
{DependencyType("unknown"), false},
}
for _, tt := range tests {
t.Run(string(tt.depType), func(t *testing.T) {
if got := tt.depType.IsWellKnown(); got != tt.wellKnown {
t.Errorf("DependencyType(%q).IsWellKnown() = %v, want %v", tt.depType, got, tt.wellKnown)
}
})
}
}
func TestDependencyTypeAffectsReadyWork(t *testing.T) {
tests := []struct {
depType DependencyType
affects bool
}{
{DepBlocks, true},
{DepParentChild, true},
{DepRelated, false},
{DepDiscoveredFrom, false},
{DepRepliesTo, false},
{DepRelatesTo, false},
{DepDuplicates, false},
{DepSupersedes, false},
{DepAuthoredBy, false},
{DepAssignedTo, false},
{DepApprovedBy, false},
{DependencyType("custom-type"), false},
}
for _, tt := range tests {
t.Run(string(tt.depType), func(t *testing.T) {
if got := tt.depType.AffectsReadyWork(); got != tt.affects {
t.Errorf("DependencyType(%q).AffectsReadyWork() = %v, want %v", tt.depType, got, tt.affects)
}
})
}
}
func TestIssueStructFields(t *testing.T) {
// Test that all time fields work correctly
now := time.Now()