Skip to content

Commit fd838f6

Browse files
authored
feat(core): SyncCapable contract — IssueSnapshot/SyncSource/SyncWriter for board sync (types only) (#87)
Adds a new sdk/core contract surface for shadow-based board synchronization, distinct from the trigger-filtered Poller/IssueEvent path. Types only; no connector implements SyncCapable yet — implementations are follow-up issues.
1 parent 0404049 commit fd838f6

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

sdk/core/api.golden

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type ChatDeps struct {
5555
Handler MessageHandler
5656
}
5757

58+
type Cursor string
59+
5860
type ExecutionChecker interface {
5961
HasCompletedExecution(taskID, projectPath string) (bool, error)
6062
InvalidateCompletion(taskID, projectPath string) error
@@ -64,12 +66,21 @@ type ExecutionSaver interface {
6466
SaveDeclinedExecution(taskID, projectPath, status, reason string) error
6567
}
6668

69+
type FieldPatch map[string]any
70+
6771
type Identity struct {
6872
UserID string
6973
DisplayName string
7074
IsBot bool
7175
}
7276

77+
type IssueDraft struct {
78+
Title string
79+
Body string
80+
Labels []string
81+
Priority string
82+
}
83+
7384
type IssueEvent struct {
7485
Action string
7586
IssueID string
@@ -102,6 +113,22 @@ type IssueResult struct {
102113
Error error
103114
}
104115

116+
type IssueSnapshot struct {
117+
NativeID string
118+
SequenceID string
119+
Title string
120+
Body string
121+
State string
122+
StateGroup string
123+
Labels []string
124+
Priority string
125+
Assignee string
126+
URL string
127+
CreatedAt time.Time
128+
UpdatedAt time.Time
129+
Deleted bool
130+
}
131+
105132
type MessageEvent struct {
106133
Action string
107134
MessageID string
@@ -181,6 +208,24 @@ type RateLimitScheduler interface {
181208
QueueRetryIfRateLimited(taskID, title, body, errText string) bool
182209
}
183210

211+
type SyncCapable interface {
212+
SyncSource
213+
SyncWriter
214+
}
215+
216+
type SyncSource interface {
217+
ListUpdatedSince(ctx context.Context, projectID string, since time.Time, page Cursor) ([]IssueSnapshot, Cursor, error)
218+
ListAll(ctx context.Context, projectID string, page Cursor) ([]IssueSnapshot, Cursor, error)
219+
GetIssue(ctx context.Context, nativeID string) (IssueSnapshot, error)
220+
}
221+
222+
type SyncWriter interface {
223+
UpdateFields(ctx context.Context, nativeID string, fields FieldPatch) (IssueSnapshot, error)
224+
TransitionState(ctx context.Context, nativeID, providerState string) error
225+
AddComment(ctx context.Context, nativeID, body, idemKey string) error
226+
CreateIssue(ctx context.Context, projectID string, draft IssueDraft) (IssueSnapshot, error)
227+
}
228+
184229
type TaskChecker interface {
185230
IsTaskQueued(taskID string) bool
186231
}

sdk/core/sync.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package core
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// IssueSnapshot is the normalized, point-in-time view of a single tracker
9+
// issue used by the board-sync engine. A connector's SyncSource produces
10+
// snapshots; the host diffs each snapshot against a stored "shadow" (the
11+
// last-synced copy) to compute a 3-way merge between the provider, the
12+
// shadow, and the host's own board state.
13+
type IssueSnapshot struct {
14+
// NativeID is the provider's opaque internal identifier for the issue.
15+
NativeID string
16+
// SequenceID is the provider-prefixed human identifier (e.g. "GH-83").
17+
// The prefix scheme is load-bearing: Pilot's branch naming
18+
// (pilot/<SequenceID>) depends on it staying stable across syncs.
19+
SequenceID string
20+
Title string
21+
Body string
22+
// State is the provider-native state id or name (e.g. a workflow state).
23+
State string
24+
// StateGroup is the provider's own state category (e.g. "todo", "in
25+
// progress", "done") for providers that expose one; empty otherwise.
26+
StateGroup string
27+
Labels []string
28+
// Priority is normalized via NormalizePriority so the shadow diff
29+
// compares one vocabulary across every provider.
30+
Priority string
31+
// Assignee is a display-only identifier; it is not resolved to a host
32+
// user record here.
33+
Assignee string
34+
URL string
35+
36+
CreatedAt time.Time
37+
UpdatedAt time.Time
38+
39+
// Deleted marks that the issue was removed or archived on the provider
40+
// side since the last sync; the shadow diff treats this as a tombstone.
41+
Deleted bool
42+
}
43+
44+
// Cursor is an opaque, provider-specific pagination token returned by
45+
// SyncSource list calls and passed back on the next page request.
46+
type Cursor string
47+
48+
// FieldPatch is a partial set of issue fields to apply via
49+
// SyncWriter.UpdateFields. Keys are field names understood by the target
50+
// connector; values are the new field content.
51+
type FieldPatch map[string]any
52+
53+
// IssueDraft is the minimal payload needed to create a new issue on a
54+
// provider via SyncWriter.CreateIssue.
55+
type IssueDraft struct {
56+
Title string
57+
Body string
58+
Labels []string
59+
Priority string
60+
}
61+
62+
// SyncSource is implemented by connectors that can read issue state for
63+
// board synchronization. Unlike Pollable/Poller, it is not trigger-label
64+
// filtered and is not restricted to created|updated actions — it exposes
65+
// the full issue set a shadow-based sync needs to reconcile.
66+
type SyncSource interface {
67+
// ListUpdatedSince returns issues changed on or after since, paginated
68+
// via page (empty Cursor requests the first page). It returns the page
69+
// of snapshots and the cursor for the next page (empty when exhausted).
70+
ListUpdatedSince(ctx context.Context, projectID string, since time.Time, page Cursor) ([]IssueSnapshot, Cursor, error)
71+
72+
// ListAll returns every issue in projectID, paginated via page. Used for
73+
// full-resync passes where an incremental cursor is unavailable or
74+
// distrusted.
75+
ListAll(ctx context.Context, projectID string, page Cursor) ([]IssueSnapshot, Cursor, error)
76+
77+
// GetIssue fetches a single issue snapshot by its native ID.
78+
GetIssue(ctx context.Context, nativeID string) (IssueSnapshot, error)
79+
}
80+
81+
// SyncWriter is implemented by connectors that can write issue state back
82+
// to the provider as part of board synchronization.
83+
type SyncWriter interface {
84+
// UpdateFields applies a partial field patch to nativeID and returns the
85+
// resulting snapshot.
86+
UpdateFields(ctx context.Context, nativeID string, fields FieldPatch) (IssueSnapshot, error)
87+
88+
// TransitionState moves nativeID to providerState, a provider-native
89+
// state id or name (see IssueSnapshot.State).
90+
TransitionState(ctx context.Context, nativeID, providerState string) error
91+
92+
// AddComment posts body as a comment on nativeID. idemKey is a caller-
93+
// supplied idempotency key so retried syncs do not double-post.
94+
AddComment(ctx context.Context, nativeID, body, idemKey string) error
95+
96+
// CreateIssue creates a new issue in projectID from draft and returns
97+
// the resulting snapshot.
98+
CreateIssue(ctx context.Context, projectID string, draft IssueDraft) (IssueSnapshot, error)
99+
}
100+
101+
// SyncCapable is implemented by connectors that support full board
102+
// synchronization: reading provider issue state (SyncSource) and writing
103+
// changes back (SyncWriter). No connector implements this yet — the types
104+
// here are the contract; connector implementations are follow-up work.
105+
type SyncCapable interface {
106+
SyncSource
107+
SyncWriter
108+
}

0 commit comments

Comments
 (0)