PushNotes/CommitNotes Updated Field Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add url and warnings to PushedNote, and an updated: [PushedNote!]! field to both PushNotesPayload and CommitNotesPayload containing only the notes changed in that push/commit.
Architecture: Three tasks: (1) schema change + gqlgen regeneration; (2) pushnotes resolver + env + tests; (3) commitnotes resolver + env + tests. The url is computed via NoteViews.ResolveFullURL (already exists). Warnings come from NoteViews.Warnings() map. Both Env interfaces get PublicURL() string. The updated slice is built by looking up each pathID in the rebuilt NoteViews after save.
Tech Stack: Go, gqlgen, moq
File Map
| File | Change |
|---|---|
internal/graph/schema.graphqls |
Add url, warnings to PushedNote; add updated to both payloads |
internal/graph/model/models_gen.go |
Regenerated by gqlgen — do not edit manually |
internal/graph/generated.go |
Regenerated by gqlgen — do not edit manually |
internal/case/pushnotes/resolve.go |
Add PublicURL to Env; update buildPushedNotes; add buildUpdatedNotes |
internal/case/pushnotes/mocks_test.go |
Regenerated by moq |
internal/case/pushnotes/resolve_test.go |
Add tests for updated, url, warnings |
internal/case/commitnotes/resolve.go |
Add PublicURL to Env; use nvs from PrepareLatestNotes; build updated |
internal/case/commitnotes/resolve_test.go |
Add tests for updated |
Task 1: Schema changes + gqlgen
Files:
-
Modify:
internal/graph/schema.graphqls -
Regenerated:
internal/graph/model/models_gen.go,internal/graph/generated.go -
Step 1: Add
urlandwarningstoPushedNotein the schema
In internal/graph/schema.graphqls, find the PushedNote type (around line 1594):
type PushedNote {
id: Int64!
path: String!
assets: [PushedNoteAsset!]!
url: String
warnings: [NoteWarning!]!
}
url is nullable (String not String!) because layout files don't have a note URL.
- Step 2: Add
updatedtoPushNotesPayload
Find PushNotesPayload (around line 1600):
type PushNotesPayload {
notes: [PushedNote!]!
updated: [PushedNote!]!
}
- Step 3: Add
updatedtoCommitNotesPayload
Find CommitNotesPayload (around line 1685):
type CommitNotesPayload {
success: Boolean!
updated: [PushedNote!]!
}
- Step 4: Run gqlgen
cd /home/alexes/projects2/trip2g && make gqlgen 2>&1
Expected: regenerates models_gen.go and generated.go. The PushedNote struct will now have:
type PushedNote struct {
ID int64 `json:"id"`
Path string `json:"path"`
Assets []PushedNoteAsset `json:"assets"`
URL *string `json:"url"`
Warnings []appmodel.NoteWarning `json:"warnings"`
}
And PushNotesPayload will have Updated []PushedNote and CommitNotesPayload will have Updated []PushedNote.
- Step 5: Build to confirm schema change compiles (expect errors in use-case files)
cd /home/alexes/projects2/trip2g && go build ./... 2>&1 | head -20
Expected: compile errors in pushnotes and commitnotes because PushNotesPayload and CommitNotesPayload structs now require Updated field. That's fine — next tasks fix them.
- Step 6: Commit schema changes
git add internal/graph/schema.graphqls internal/graph/model/models_gen.go internal/graph/generated.go
git commit -m "feat(graphql): add url, warnings, updated to PushedNote and push/commit payloads"
Task 2: Update pushnotes resolver
Files:
-
Modify:
internal/case/pushnotes/resolve.go -
Regenerate:
internal/case/pushnotes/mocks_test.go -
Modify:
internal/case/pushnotes/resolve_test.go -
Step 1: Add
PublicURL() stringto the pushnotesEnvinterface
In internal/case/pushnotes/resolve.go, add PublicURL to the Env interface:
type Env interface {
Logger() logger.Logger
InsertNote(ctx context.Context, update appmodel.RawNote) (int64, error)
InsertUncommittedPath(ctx context.Context, notePathID int64) error
PrepareLatestNotes(ctx context.Context, partial bool) (*appmodel.NoteViews, error)
HandleLatestNotesAfterSave(ctx context.Context, changedPathIDs []int64) error
Layouts() *appmodel.Layouts
LatestNoteViews() *appmodel.NoteViews
CheckStorageLimits(ctx context.Context, additionalAssetBytes int64) (string, error)
PublicURL() string
}
- Step 2: Update
buildPushedNotesto populateurlandwarnings
Replace the existing buildPushedNotes function:
func buildPushedNotes(nvs *appmodel.NoteViews, layouts *appmodel.Layouts, publicURL string) []model.PushedNote {
warnings := nvs.Warnings()
pushedNotes := []model.PushedNote{}
for _, note := range nvs.List {
assets := buildNoteAssets(note)
urlVal := nvs.ResolveFullURL(note, publicURL)
noteWarnings := warnings[note.Path]
if noteWarnings == nil {
noteWarnings = []appmodel.NoteWarning{}
}
pushedNotes = append(pushedNotes, model.PushedNote{
ID: note.VersionID,
Path: note.Path,
Assets: assets,
URL: &urlVal,
Warnings: noteWarnings,
})
}
for _, layout := range layouts.Map {
assets := buildLayoutAssets(layout)
pushedNotes = append(pushedNotes, model.PushedNote{
ID: layout.VersionID,
Path: layout.Path,
Assets: assets,
URL: nil,
Warnings: []appmodel.NoteWarning{},
})
}
return pushedNotes
}
- Step 3: Add
buildUpdatedNotesfunction
Add after buildPushedNotes:
// buildUpdatedNotes returns PushedNote entries for only the notes with the given pathIDs.
// Notes not found in nvs (e.g. deleted) are silently skipped.
func buildUpdatedNotes(nvs *appmodel.NoteViews, pathIDs []int64, publicURL string) []model.PushedNote {
warnings := nvs.Warnings()
result := make([]model.PushedNote, 0, len(pathIDs))
for _, id := range pathIDs {
note := nvs.GetByPathID(id)
if note == nil {
continue
}
urlVal := nvs.ResolveFullURL(note, publicURL)
noteWarnings := warnings[note.Path]
if noteWarnings == nil {
noteWarnings = []appmodel.NoteWarning{}
}
result = append(result, model.PushedNote{
ID: note.VersionID,
Path: note.Path,
Assets: buildNoteAssets(note),
URL: &urlVal,
Warnings: noteWarnings,
})
}
return result
}
- Step 4: Update
Resolveto use new functions and populateUpdated
In the Resolve function, update both return sites:
Early return (no updates):
if len(input.Updates) == 0 {
nvs := env.LatestNoteViews()
pushedNotes := buildPushedNotes(nvs, env.Layouts(), env.PublicURL())
return &model.PushNotesPayload{Notes: pushedNotes, Updated: []model.PushedNote{}}, nil
}
Normal return (after save):
pushedNotes := buildPushedNotes(nvs, env.Layouts(), env.PublicURL())
updatedNotes := buildUpdatedNotes(nvs, pathIDs, env.PublicURL())
return &model.PushNotesPayload{Notes: pushedNotes, Updated: updatedNotes}, nil
- Step 5: Regenerate pushnotes mock
cd /home/alexes/projects2/trip2g/internal/case/pushnotes && go generate .
This regenerates mocks_test.go adding PublicURLFunc func() string.
- Step 6: Write failing unit tests
Add to internal/case/pushnotes/resolve_test.go, inside TestResolve table or as a separate test function:
func TestResolve_UpdatedNotes(t *testing.T) {
ctx := context.Background()
mockLogger := &logger.TestLogger{}
makeNVS := func() *appmodel.NoteViews {
nvs := appmodel.NewNoteViews()
note := &appmodel.NoteView{
PathID: 42,
VersionID: 1,
Path: "my-note.md",
Permalink: "/my-note",
}
note.AddWarning(appmodel.NoteWarningWarning, "broken link to [[missing]]")
nvs.RegisterNote(note)
return nvs
}
t.Run("updated contains pushed notes with url and warnings", func(t *testing.T) {
env := newEnvMock(mockLogger)
env.InsertNoteFunc = func(_ context.Context, _ appmodel.RawNote) (int64, error) {
return 42, nil
}
env.PrepareLatestNotesFunc = func(_ context.Context, _ bool) (*appmodel.NoteViews, error) {
return makeNVS(), nil
}
env.HandleLatestNotesAfterSaveFunc = func(_ context.Context, _ []int64) error {
return nil
}
env.LayoutsFunc = func() *appmodel.Layouts {
return &appmodel.Layouts{Map: map[string]appmodel.Layout{}}
}
env.PublicURLFunc = func() string { return "https://example.com" }
input := model.PushNotesInput{
Updates: []model.PushNoteInput{
{Path: "my-note.md", Content: "# Hello"},
},
}
result, err := pushnotes.Resolve(ctx, env, input)
require.NoError(t, err)
payload, ok := result.(*model.PushNotesPayload)
require.True(t, ok)
// updated contains only the pushed note
require.Len(t, payload.Updated, 1)
require.Equal(t, "my-note.md", payload.Updated[0].Path)
require.NotNil(t, payload.Updated[0].URL)
require.Equal(t, "https://example.com/my-note", *payload.Updated[0].URL)
require.Len(t, payload.Updated[0].Warnings, 1)
require.Equal(t, "broken link to [[missing]]", payload.Updated[0].Warnings[0].Message)
// notes (all) also has url populated
require.Len(t, payload.Notes, 1)
require.NotNil(t, payload.Notes[0].URL)
require.Equal(t, "https://example.com/my-note", *payload.Notes[0].URL)
})
t.Run("updated is empty when no updates provided", func(t *testing.T) {
env := newEnvMock(mockLogger)
env.LatestNoteViewsFunc = func() *appmodel.NoteViews {
return makeNVS()
}
env.LayoutsFunc = func() *appmodel.Layouts {
return &appmodel.Layouts{Map: map[string]appmodel.Layout{}}
}
env.PublicURLFunc = func() string { return "https://example.com" }
input := model.PushNotesInput{Updates: []model.PushNoteInput{}}
result, err := pushnotes.Resolve(ctx, env, input)
require.NoError(t, err)
payload, ok := result.(*model.PushNotesPayload)
require.True(t, ok)
require.Empty(t, payload.Updated)
})
}
- Step 7: Run tests — expect failures first
cd /home/alexes/projects2/trip2g && go test ./internal/case/pushnotes/... -run TestResolve_UpdatedNotes -v 2>&1
Expected: FAIL (functions not yet updated or missing PublicURLFunc).
- Step 8: Run tests — verify they pass after implementation
cd /home/alexes/projects2/trip2g && go test ./internal/case/pushnotes/... -v 2>&1 | tail -15
Expected: all PASS.
- Step 9: Build
cd /home/alexes/projects2/trip2g && go build ./... 2>&1
- Step 10: Commit
git add internal/case/pushnotes/resolve.go internal/case/pushnotes/mocks_test.go internal/case/pushnotes/resolve_test.go
git commit -m "feat(pushnotes): populate url, warnings, and updated in PushNotesPayload"
Task 3: Update commitnotes resolver
Files:
-
Modify:
internal/case/commitnotes/resolve.go -
Modify:
internal/case/commitnotes/resolve_test.go(create if missing) -
Step 1: Add
PublicURL() stringto commitnotesEnv
In internal/case/commitnotes/resolve.go, update the Env interface:
type Env interface {
PrepareLatestNotes(ctx context.Context, partial bool) (*appmodel.NoteViews, error)
HandleLatestNotesAfterSave(ctx context.Context, changedPathIDs []int64) error
ListUncommittedPaths(ctx context.Context) ([]int64, error)
ClearUncommittedPaths(ctx context.Context) error
PublicURL() string
}
- Step 2: Update
Resolveto use nvs and buildupdated
Replace the full Resolve function:
func Resolve(ctx context.Context, env Env) (Payload, error) {
pathIDs, err := env.ListUncommittedPaths(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list uncommitted paths: %w", err)
}
nvs, err := env.PrepareLatestNotes(ctx, false)
if err != nil {
return nil, fmt.Errorf("failed to prepare notes: %w", err)
}
err = env.HandleLatestNotesAfterSave(ctx, pathIDs)
if err != nil {
return nil, fmt.Errorf("failed to handle latest notes after save: %w", err)
}
err = env.ClearUncommittedPaths(ctx)
if err != nil {
return nil, fmt.Errorf("failed to clear uncommitted paths: %w", err)
}
updated := buildUpdatedNotes(nvs, pathIDs, env.PublicURL())
return &model.CommitNotesPayload{Success: true, Updated: updated}, nil
}
- Step 3: Add
buildUpdatedNotesto commitnotes
Add this function in internal/case/commitnotes/resolve.go (same logic as pushnotes, but standalone):
func buildUpdatedNotes(nvs *appmodel.NoteViews, pathIDs []int64, publicURL string) []model.PushedNote {
warnings := nvs.Warnings()
result := make([]model.PushedNote, 0, len(pathIDs))
for _, id := range pathIDs {
note := nvs.GetByPathID(id)
if note == nil {
continue
}
urlVal := nvs.ResolveFullURL(note, publicURL)
noteWarnings := warnings[note.Path]
if noteWarnings == nil {
noteWarnings = []appmodel.NoteWarning{}
}
result = append(result, model.PushedNote{
ID: note.VersionID,
Path: note.Path,
Assets: []model.PushedNoteAsset{},
URL: &urlVal,
Warnings: noteWarnings,
})
}
return result
}
Note: Assets is an empty slice (not nil) to satisfy [PushedNoteAsset!]!. Commitnotes doesn't process assets — use cases should not build asset info they don't have.
- Step 4: Check if commitnotes has a test file
ls /home/alexes/projects2/trip2g/internal/case/commitnotes/
If resolve_test.go doesn't exist, create it. If it does, add to it.
- Step 5: Write failing unit test for commitnotes
Create or add to internal/case/commitnotes/resolve_test.go:
package commitnotes_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"trip2g/internal/case/commitnotes"
"trip2g/internal/graph/model"
appmodel "trip2g/internal/model"
)
//go:generate go run github.com/matryer/moq -out mocks_test.go -pkg commitnotes_test . Env
type Env interface {
PrepareLatestNotes(ctx context.Context, partial bool) (*appmodel.NoteViews, error)
HandleLatestNotesAfterSave(ctx context.Context, changedPathIDs []int64) error
ListUncommittedPaths(ctx context.Context) ([]int64, error)
ClearUncommittedPaths(ctx context.Context) error
PublicURL() string
}
func TestResolve_Updated(t *testing.T) {
ctx := context.Background()
makeNVS := func() *appmodel.NoteViews {
nvs := appmodel.NewNoteViews()
note := &appmodel.NoteView{
PathID: 7,
VersionID: 3,
Path: "commit-note.md",
Permalink: "/commit-note",
}
nvs.RegisterNote(note)
return nvs
}
env := &EnvMock{
ListUncommittedPathsFunc: func(_ context.Context) ([]int64, error) {
return []int64{7}, nil
},
PrepareLatestNotesFunc: func(_ context.Context, _ bool) (*appmodel.NoteViews, error) {
return makeNVS(), nil
},
HandleLatestNotesAfterSaveFunc: func(_ context.Context, _ []int64) error {
return nil
},
ClearUncommittedPathsFunc: func(_ context.Context) error {
return nil
},
PublicURLFunc: func() string { return "https://site.com" },
}
result, err := commitnotes.Resolve(ctx, env)
require.NoError(t, err)
payload, ok := result.(*model.CommitNotesPayload)
require.True(t, ok)
require.True(t, payload.Success)
require.Len(t, payload.Updated, 1)
require.Equal(t, "commit-note.md", payload.Updated[0].Path)
require.NotNil(t, payload.Updated[0].URL)
require.Equal(t, "https://site.com/commit-note", *payload.Updated[0].URL)
require.Empty(t, payload.Updated[0].Warnings)
}
- Step 6: Generate commitnotes mock
cd /home/alexes/projects2/trip2g/internal/case/commitnotes && go generate .
If no //go:generate directive exists yet (check first), run manually:
cd /home/alexes/projects2/trip2g/internal/case/commitnotes && go run github.com/matryer/moq -out mocks_test.go -pkg commitnotes_test . Env
- Step 7: Run commitnotes tests
cd /home/alexes/projects2/trip2g && go test ./internal/case/commitnotes/... -v 2>&1
Expected: all PASS.
- Step 8: Run full build and all related tests
cd /home/alexes/projects2/trip2g && go build ./... 2>&1
cd /home/alexes/projects2/trip2g && go test ./internal/case/pushnotes/... ./internal/case/commitnotes/... -v 2>&1 | tail -10
Expected: clean build, all tests PASS.
- Step 9: Commit
git add internal/case/commitnotes/resolve.go internal/case/commitnotes/resolve_test.go internal/case/commitnotes/mocks_test.go
git commit -m "feat(commitnotes): populate url, warnings, and updated in CommitNotesPayload"