Skip to content

refactor: coordination rebuild + schema normalization#6

Merged
Koopa0 merged 1 commit into
mainfrom
refactor/schema-normalization
Apr 15, 2026
Merged

refactor: coordination rebuild + schema normalization#6
Koopa0 merged 1 commit into
mainfrom
refactor/schema-normalization

Conversation

@Koopa0
Copy link
Copy Markdown
Owner

@Koopa0 Koopa0 commented Apr 15, 2026

Big-bang refactor covering three tracks:

  1. Coordination layer rebuild — replaces directive/report/journal/insight/ task/synthesis/consolidation packages with the new coordination model: agent + todo + agent/note + hypothesis + obsidian/note + weekly. Vocabulary discipline: task = inter-agent coordination, todo = personal GTD.

  2. Schema normalization — migration 001 rewrite for naming precision:

    • todo_items → todos, learning_items → learning_targets, learning_plan_items → learning_plan_entries, notes → obsidian_notes
    • notion_page_id/notion_task_id → external_provider + external_ref pair across goals/milestones/projects/todos/obsidian_notes
    • obsidian_notes.source → provenance, agent_notes.source → author, hypotheses.source → author, contents.source → origin_ref, tasks.source/target → requester/assignee
    • learning_target_relations.source_item_id/target_item_id → anchor_id/related_id
    • CHECK constraints: slug formats, email, URL schemes, hash formats, non-empty text fields
    • learning_sessions.updated_at added; bookmarks.curated_by FK to agents; legacy_content_id dropped
  3. sqlc full coverage — zero raw SQL outside internal/db/ and tests. Converted agent, bookmark, stats, commitment area lookup, feed collection stats. Added internal/stats/query.sql with 25 Stats* queries.

Verification: go build / vet / test all green; touched packages have 0 new lint issues (pre-existing repo-wide issues in untouched packages unchanged).

Big-bang refactor covering three tracks:

1. Coordination layer rebuild — replaces directive/report/journal/insight/
   task/synthesis/consolidation packages with the new coordination model:
   agent + todo + agent/note + hypothesis + obsidian/note + weekly. Vocabulary
   discipline: task = inter-agent coordination, todo = personal GTD.

2. Schema normalization — migration 001 rewrite for naming precision:
   - todo_items → todos, learning_items → learning_targets,
     learning_plan_items → learning_plan_entries, notes → obsidian_notes
   - notion_page_id/notion_task_id → external_provider + external_ref pair
     across goals/milestones/projects/todos/obsidian_notes
   - obsidian_notes.source → provenance, agent_notes.source → author,
     hypotheses.source → author, contents.source → origin_ref,
     tasks.source/target → requester/assignee
   - learning_target_relations.source_item_id/target_item_id → anchor_id/related_id
   - CHECK constraints: slug formats, email, URL schemes, hash formats,
     non-empty text fields
   - learning_sessions.updated_at added; bookmarks.curated_by FK to agents;
     legacy_content_id dropped

3. sqlc full coverage — zero raw SQL outside internal/db/ and tests.
   Converted agent, bookmark, stats, commitment area lookup, feed collection
   stats. Added internal/stats/query.sql with 25 Stats* queries.

Verification: go build / vet / test all green; touched packages have 0 new
lint issues (pre-existing repo-wide issues in untouched packages unchanged).
@Koopa0 Koopa0 merged commit a261bc8 into main Apr 15, 2026
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Apr 15, 2026

This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment augment review and we will review it.

@Koopa0 Koopa0 deleted the refactor/schema-normalization branch April 15, 2026 03:37
Comment thread cmd/mcp/main.go
Comment on lines +58 to +59
if syncErr != nil {
return fmt.Errorf("syncing agent registry: %w", syncErr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If agent.SyncToTable returns an error, it is immediately returned, but the error is not logged. This can make debugging startup failures more difficult, especially in production environments where returned errors may not be visible.

Recommendation: Log the sync error before returning:

if syncErr != nil {
    logger.Error("agent registry sync failed", "error", syncErr)
    return fmt.Errorf("syncing agent registry: %w", syncErr)
}

Comment thread cmd/mcp/main.go
Comment on lines 72 to 76
server := mcp.NewServer(pool, logger,
mcp.WithLocation(taipeiLoc),
mcp.WithParticipant(cfg.Participant),
mcp.WithRegistry(agentRegistry),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The construction of the MCP server via mcp.NewServer does not validate the provided dependencies (e.g., pool, logger, taipeiLoc, cfg.Participant, agentRegistry). If any of these are nil or invalid due to misconfiguration, it could result in a runtime panic or undefined behavior later.

Recommendation: Add explicit validation for critical dependencies before server instantiation, or ensure that mcp.NewServer itself performs such validation and returns an error if any required argument is invalid.

-- Used for double-complete detection: count today's todo_completed events for a specific todo item.
SELECT count(*)::int FROM events
WHERE event_type = @event_type
AND source_id LIKE @source_prefix || '%'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of source_id LIKE @source_prefix || '%' may result in a full table scan if there is no suitable index on source_id for prefix searches. This can significantly degrade performance for large tables.

Recommendation: Ensure there is a B-tree index on source_id or consider using the pg_trgm extension for more efficient prefix matching if the column is not already indexed appropriately.

Comment thread internal/admin/admin.go
reports: report.NewStore(pool),
synth: synthesis.NewStore(pool),
loc: loc,
logger: logger,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger not validated for nil:

The logger field is assigned directly without checking for nil. If a nil logger is passed and later used, it could cause a runtime panic. Consider validating the logger parameter or providing a default logger to ensure robustness.

Recommended solution:

if logger == nil {
    logger = slog.Default()
}

Comment on lines 83 to 84
if err != nil {
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error handling:

The error from h.todos.CompletedItemsDetailSince is silently ignored by returning early. This can obscure operational issues and make debugging difficult. Consider logging the error or propagating it to the caller to ensure failures are visible and actionable.

Recommended solution:

if err != nil {
    // Log the error or propagate it
    log.Printf("failed to fetch completed items: %v", err)
    return
}

-- name: CreateAgentNote :one
-- Insert an agent note entry.
INSERT INTO agent_notes (kind, author, content, metadata, entry_date)
VALUES (@kind::agent_note_kind, @author, @content, @metadata, @entry_date)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata Field Validation

The CreateAgentNote query (line 4) inserts the metadata field. If metadata is intended to be JSON, ensure that the application layer validates the JSON structure before insertion to prevent malformed data. This will help maintain data integrity and avoid downstream parsing errors.

Comment on lines +26 to +47
func (s *Store) Create(ctx context.Context, p *CreateParams) (*Note, error) {
var metaBytes json.RawMessage
if p.Metadata != nil {
b, err := json.Marshal(p.Metadata)
if err != nil {
return nil, fmt.Errorf("marshaling metadata: %w", err)
}
metaBytes = b
}

r, err := s.q.CreateAgentNote(ctx, db.CreateAgentNoteParams{
Kind: db.AgentNoteKind(p.Kind),
Author: p.Author,
Content: p.Content,
Metadata: metaBytes,
EntryDate: p.EntryDate,
})
if err != nil {
return nil, fmt.Errorf("creating agent note: %w", err)
}
return rowToNote(&r)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lack of Input Validation in Create

The Create method does not validate required fields in the CreateParams struct (such as Kind, Author, or Content). This could allow invalid or incomplete data to be inserted into the database, potentially causing downstream errors or violating data integrity constraints.

Recommendation:
Add explicit validation for required fields before attempting to marshal metadata or insert into the database. For example:

if p.Kind == "" || p.Author == "" || p.Content == "" {
    return nil, errors.New("missing required fields: kind, author, or content")
}

Comment on lines +62 to +86
func (s *Store) NotesInRange(ctx context.Context, start, end time.Time, kindFilter *Kind, sourceFilter *string) ([]Note, error) {
kindArg := db.NullAgentNoteKind{}
if kindFilter != nil {
kindArg.AgentNoteKind = db.AgentNoteKind(*kindFilter)
kindArg.Valid = true
}
rows, err := s.q.AgentNotesByDateRange(ctx, db.AgentNotesByDateRangeParams{
StartDate: start,
EndDate: end,
Kind: kindArg,
Author: sourceFilter,
})
if err != nil {
return nil, fmt.Errorf("listing agent notes in range: %w", err)
}
notes := make([]Note, 0, len(rows))
for i := range rows {
n, err := rowToNote(&rows[i])
if err != nil {
return nil, err
}
notes = append(notes, *n)
}
return notes, nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ambiguous Handling of sourceFilter in NotesInRange

In the NotesInRange method, the sourceFilter parameter is passed directly as the Author argument to the query. If sourceFilter is nil, the behavior depends on the underlying query implementation. This could result in returning all authors instead of filtering, which may not be the intended behavior.

Recommendation:
Clarify and document the behavior when sourceFilter is nil. If the intention is to return all authors when sourceFilter is not provided, ensure the underlying query handles this explicitly. Otherwise, add logic to prevent unintended broad queries.

Comment on lines +181 to +188
func (r *Registry) SetStatus(name Name, s Status) {
r.mu.Lock()
defer r.mu.Unlock()
if a, ok := r.byName[name]; ok {
a.Status = s
r.byName[name] = a
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential for Incorrect Usage of SetStatus After Startup

The SetStatus method is documented as safe to call only during startup, but there is no enforcement of this constraint. If SetStatus is inadvertently called after HTTP serving begins, it could lead to inconsistent state or subtle concurrency bugs, since the registry is intended to be read-only after initialization.

Recommendation:
Consider adding a mechanism to enforce or detect post-startup calls, such as a boolean flag (e.g., locked) that is set after initialization, and panic or return an error if SetStatus is called when not allowed. This will help prevent accidental misuse.

Comment on lines +190 to +197
func sortAgentsByName(agents []Agent) {
// Small N — insertion sort avoids pulling in sort.Slice just for this.
for i := 1; i < len(agents); i++ {
for j := i; j > 0 && agents[j-1].Name > agents[j].Name; j-- {
agents[j-1], agents[j] = agents[j], agents[j-1]
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insertion Sort May Not Scale for Large Agent Lists

The sortAgentsByName function uses insertion sort, which is efficient for small lists but has O(n^2) complexity. If the number of agents increases, this could become a performance bottleneck.

Recommendation:
If scalability is a concern, consider replacing this with Go's built-in sort.Slice for better performance on larger lists:

sort.Slice(agents, func(i, j int) bool { return agents[i].Name < agents[j].Name })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant