Documentation
¶
Overview ¶
Package store owns the SQLite mirror: schema, migrations, transactions, full-text index and the derived fields the source does not provide.
0.x promises three things, documented in specs/000-product/data-model.md: the issues_full view plus the RECIPES queries, gadak sql stdout, and views open --keys - semantics. Everything else in the schema is documented so you can read it, not promised so you can build on it.
Index ¶
- Constants
- Variables
- func EnsureLocal(mirrorPath string) error
- func LocalAttachReuses() uint64
- func LocalNewerSchemaWarnsSuppressed() uint64
- func LocalPath(mirrorPath string) string
- func Now() string
- func OpenReadOnly(path string) (*sql.DB, error)
- type APIUsageDay
- type APIUsageDelta
- type APIUsageSum
- type APIUsageSummary
- type Attachment
- type AuthorComment
- type Batch
- type ChangeEntry
- type Comment
- type CommentsByAuthorResult
- type DB
- func (db *DB) APIUsage(ctx context.Context, days int) ([]APIUsageDay, error)
- func (db *DB) APIUsageSummary(ctx context.Context) (APIUsageSummary, error)
- func (db *DB) AbsorbRecents(ctx context.Context, kinds map[string][]string) error
- func (db *DB) AddAPIUsage(ctx context.Context, day string, u APIUsageDelta) error
- func (db *DB) AppendSyncRun(ctx context.Context, sourceID string, r SyncRun) error
- func (db *DB) AttachmentBelongs(ctx context.Context, issueKey, attachmentID string) (bool, error)
- func (db *DB) AttachmentOrigin(ctx context.Context, issueKey, attachmentID string) (sourceID, contentURL string, err error)
- func (db *DB) Close() error
- func (db *DB) CommentsByAuthor(ctx context.Context, authorID string, limit int) (CommentsByAuthorResult, error)
- func (db *DB) ComputeFieldUsage(ctx context.Context, aliases []string) ([]FieldUsageRow, error)
- func (db *DB) ConfluenceSpaceWatermarks(ctx context.Context, sourceID string) (map[string]string, error)
- func (db *DB) DeleteItems(ctx context.Context, sourceID string, keys []string) (int, error)
- func (db *DB) DeleteSavedView(ctx context.Context, id string) error
- func (db *DB) DeletedKeysSince(ctx context.Context, since string) ([]string, error)
- func (db *DB) Detail(ctx context.Context, key string) (*Detail, error)
- func (db *DB) DistinctCount(ctx context.Context, table, column string) (int, error)
- func (db *DB) DropSourceMirror(ctx context.Context, sourceID string) error
- func (db *DB) EnrichmentsByKind(ctx context.Context, kind string) (map[string]json.RawMessage, error)
- func (db *DB) EnrichmentsFor(ctx context.Context, key string) (map[string]json.RawMessage, error)
- func (db *DB) Favorites(ctx context.Context) ([]string, error)
- func (db *DB) Feed(ctx context.Context, opts FeedOpts) (FeedResult, error)
- func (db *DB) FieldUsage(ctx context.Context) ([]FieldUsageRow, error)
- func (db *DB) FreshenSyncClock(ctx context.Context) error
- func (db *DB) GroupQueryHits(ctx context.Context, query string) (map[string]string, error)
- func (db *DB) HasCustomFieldKeysInRaw(ctx context.Context) (bool, error)
- func (db *DB) HasPageVersion(ctx context.Context, itemID string, number int) (bool, error)
- func (db *DB) HasSource(ctx context.Context, id string) (bool, error)
- func (db *DB) History(ctx context.Context, opts HistoryOpts) (HistoryPage, error)
- func (db *DB) ImportRecents(ctx context.Context, items []Recent) error
- func (db *DB) IssueLites(ctx context.Context) ([]IssueLite, error)
- func (db *DB) IssueLitesByKeys(ctx context.Context, keys []string) ([]IssueLite, error)
- func (db *DB) IssueLitesSince(ctx context.Context, since string) ([]IssueLite, error)
- func (db *DB) KeySource(ctx context.Context, key string) (string, error)
- func (db *DB) KeysBySource(ctx context.Context, sourceID string) ([]string, error)
- func (db *DB) MarkFeedRead(ctx context.Context, opts MarkFeedReadOpts) (MarkFeedReadResult, error)
- func (db *DB) PageDetail(ctx context.Context, key string) (*PageDetail, error)
- func (db *DB) PageLites(ctx context.Context) ([]PageLite, error)
- func (db *DB) PageStamps(ctx context.Context, sourceID, spaceKey string) (map[string]PageStamp, error)
- func (db *DB) PageVersions(ctx context.Context, itemID string) ([]PageVersion, error)
- func (db *DB) PoolStats() sql.DBStats
- func (db *DB) PruneConfluenceSpaces(ctx context.Context, sourceID string, keepKeys []string) (int, error)
- func (db *DB) PruneLocalHistory(ctx context.Context) error
- func (db *DB) PurgeIssueIDsOutsideNamespace(ctx context.Context, sourceID, ns string) (int, error)
- func (db *DB) PurgePageIDsOutsideNamespace(ctx context.Context, sourceID, ns string) (int, error)
- func (db *DB) PutSavedView(ctx context.Context, v SavedView) error
- func (db *DB) Recents(ctx context.Context, kind string) ([]Recent, error)
- func (db *DB) RecordRecent(ctx context.Context, kind, value string) (Recent, error)
- func (db *DB) RecordSearch(ctx context.Context, query string, resultCount int, ...) (Search, error)
- func (db *DB) RecordSync(ctx context.Context, sourceID string, r SyncResult) error
- func (db *DB) RecordVisit(ctx context.Context, kind, key string) (Visit, error)
- func (db *DB) ReingestCustom(ctx context.Context, specs []fields.SpecIDs, bodyFieldIDs []string) (int, error)
- func (db *DB) ReplaceFieldUsage(ctx context.Context, rows []FieldUsageRow) error
- func (db *DB) ReplacePageVersions(ctx context.Context, itemID string, vers []PageVersion) error
- func (db *DB) ReplaceSourceQueries(ctx context.Context, sourceID string, queries []SourceQuery) error
- func (db *DB) SavedViews(ctx context.Context) ([]SavedView, error)
- func (db *DB) ScanFieldFill(ctx context.Context, ...) error
- func (db *DB) SchemaVersion() int
- func (db *DB) Search(ctx context.Context, query string, limit int) (SearchResult, error)
- func (db *DB) SearchExplain(ctx context.Context, query string, limit int) (SearchResult, error)
- func (db *DB) SetFavorite(ctx context.Context, key string, on bool) error
- func (db *DB) SetLastNotifiedAt(ctx context.Context, sourceID, at string) error
- func (db *DB) SetSearchOpened(ctx context.Context, id int64, kind, key string) (Search, error)
- func (db *DB) SetSpaceWatermark(ctx context.Context, sourceID, key, watermark string) error
- func (db *DB) SetWatch(ctx context.Context, key string, on bool) error
- func (db *DB) SourceQueries(ctx context.Context, sourceID string) ([]SourceQuery, error)
- func (db *DB) SyncRuns(ctx context.Context, sourceID string, limit int) ([]SyncRun, error)
- func (db *DB) SyncState(ctx context.Context, sourceID string) (SyncState, error)
- func (db *DB) TableCount(ctx context.Context, table string) (int, error)
- func (db *DB) UpsertIssues(ctx context.Context, b Batch) (int, error)
- func (db *DB) UpsertPages(ctx context.Context, records []PageRecord) (int, error)
- func (db *DB) UpsertSource(ctx context.Context, s Source) error
- func (db *DB) UpsertSpaces(ctx context.Context, sourceID string, rows []SpaceRow) error
- func (db *DB) Watches(ctx context.Context) ([]string, error)
- func (db *DB) WriteBusyRetries() uint64
- type DeriveInput
- type Derived
- type Detail
- type DetailAttachment
- type DetailChange
- type DetailComment
- type DetailLink
- type FeedFocus
- type FeedIdentity
- type FeedItem
- type FeedOpts
- type FeedResult
- type FeedUnreadCounts
- type FieldUsageRow
- type HistoryItem
- type HistoryOpts
- type HistoryPage
- type Issue
- type IssueLite
- type IssueRecord
- type Item
- type ItemRef
- type Link
- type MarkFeedReadOpts
- type MarkFeedReadResult
- type Page
- type PageComment
- type PageDetail
- type PageLite
- type PageRecord
- type PageStamp
- type PageVersion
- type Recent
- type SavedView
- type Search
- type SearchExplain
- type SearchMatch
- type SearchResult
- type Source
- type SourceQuery
- type SpaceRow
- type SyncResult
- type SyncRun
- type SyncState
- type Visit
Constants ¶
const ( FeedWindowDays = 30 FeedDefaultLimit = 80 FeedMaxLimit = 200 )
Feed window and default page size match the client contract (web/src/lib/api.ts getFeed / FeedFocus).
const ( VisitKindIssue = "issue" VisitKindPage = "page" )
VisitKind is visits.kind / searches.opened_kind: "issue" or "page".
const ( CommentsByAuthorDefaultLimit = 50 CommentsByAuthorMaxLimit = 200 )
People-axis comment list defaults (GET people/{author_id}/comments/).
const CategoryDone = "done"
CategoryDone is the only status category with meaning to the derived rules. Every rule keys on the category, never on a status name: the internal tool this was extracted from matched "Reopened" / "다시 열림" and broke on any other site or account language (contracts/sync.md, "Localization hazard").
const LocalRetention = 180 * 24 * time.Hour
LocalRetention is how long raw visit/search events are kept. Counts older than this window are not rolled up — derive from whatever is still here.
const RecentCap = 10
RecentCap is the per-kind ceiling. Matches web/src/lib/recency.ts MAX.
Variables ¶
var ErrKeyAmbiguous = errors.New("this key is mirrored from more than one source — scope one side out (projects / linear.teamIds) before writing to it")
ErrKeyAmbiguous means one key exists under more than one source (a Jira project ENG and a Linear team key ENG both mint ENG-1). A write routed by that key could land on the wrong tracker while the UI shows the other row (GDK-400), so callers refuse instead of picking one.
var ErrNotFound = errors.New("not found")
ErrNotFound means a single-row lookup (Detail, etc.) found no matching key. Callers map it to 404 / "not in the mirror"; store code still uses sql.ErrNoRows internally and wraps at the package boundary.
Functions ¶
func EnsureLocal ¶ added in v0.13.0
EnsureLocal creates local.db next to the mirror if needed and migrates it. Failures are returned; callers of Open log them and still open the mirror.
func LocalAttachReuses ¶ added in v0.16.0
func LocalAttachReuses() uint64
LocalAttachReuses is how many times attachLocalHook found schema `local` already present and skipped the failure log.
func LocalNewerSchemaWarnsSuppressed ¶ added in v0.16.0
func LocalNewerSchemaWarnsSuppressed() uint64
LocalNewerSchemaWarnsSuppressed is how many times migrateLocal skipped a repeat of the newer-schema notice for a path already warned.
Types ¶
type APIUsageDay ¶
type APIUsageDay struct {
Day string `json:"day"`
Requests int64 `json:"requests"`
Throttled int64 `json:"throttled"`
ServerErrors int64 `json:"server_errors"`
Retries int64 `json:"retries"`
WaitMS int64 `json:"wait_ms"`
LastThrottledAt *string `json:"last_throttled_at,omitempty"`
}
APIUsageDay is one row of daily accumulated outbound API volume.
type APIUsageDelta ¶
type APIUsageDelta struct {
Requests int64
Throttled int64
ServerErrors int64
Retries int64
WaitMS int64
LastThrottledAt string // RFC3339 UTC, empty if unknown this flush
}
APIUsageDelta is a non-negative increment to accumulate into one UTC day. Numbers only — this package must not import jira (layering).
type APIUsageSum ¶
type APIUsageSum struct {
Requests int64 `json:"requests"`
Throttled int64 `json:"throttled"`
ServerErrors int64 `json:"server_errors"`
Retries int64 `json:"retries"`
WaitMS int64 `json:"wait_ms"`
LastThrottledAt *string `json:"last_throttled_at,omitempty"`
}
APIUsageSum is a multi-day total (no day key).
type APIUsageSummary ¶
type APIUsageSummary struct {
Today APIUsageDay `json:"today"`
Last7Days APIUsageSum `json:"last_7_days"`
}
APIUsageSummary is the shape status --json and GET settings/ runtime expose: today's row plus a 7-day rollup of our outbound call volume.
type Attachment ¶
type Attachment struct {
ID string
ExternalID string
Filename string
MimeType string
Size int64
Author string
AuthorID string
CreatedAt string
// URL is the origin content URL when the source does not share Jira's
// /attachment/content/{id} shape. Empty for Jira (the proxy builds it).
URL string
}
Attachment is metadata only. Bytes are proxied on demand, never mirrored.
type AuthorComment ¶
type AuthorComment struct {
Key string `json:"key"`
Kind string `json:"kind"` // "issue" | "page"
Title string `json:"title"`
Snippet string `json:"snippet"`
CreatedAt string `json:"created_at"`
}
AuthorComment is one row in GET people/{author_id}/comments/.
type Batch ¶
type Batch struct {
Categories map[string]string // status id -> status category
Priorities []string // priority display names, most urgent first
Records []IssueRecord
// Force rewrites rows whose updated_at is unchanged. Off, an unchanged row
// is skipped entirely, which is what keeps an incremental re-run from
// bumping sync_state.version.
Force bool
}
Batch is one page of sync output. Categories and Priorities come from the site's own metadata endpoints (which are not localized) and feed the derived field rules.
type ChangeEntry ¶
type ChangeEntry struct {
ID string
At string
Author string
AuthorID string
Field string // "status", "assignee", "priority", ...
FromValue string
FromID string
ToValue string
ToID string
}
ChangeEntry is one field change. Status entries carry ids because display names are localized per account.
type Comment ¶
type Comment struct {
ID string // "<source_id>:<comment_id>"
ExternalID string
Author string
AuthorID string
BodyADF json.RawMessage
BodyText string
CreatedAt string
UpdatedAt string
}
Comment is stored flat: the source API exposes no thread parent.
type CommentsByAuthorResult ¶
type CommentsByAuthorResult struct {
// Author is the display name from the newest matching comment ("" when none).
Author string `json:"author"`
Total int `json:"total"`
Comments []AuthorComment `json:"comments"`
}
CommentsByAuthorResult is the response body for comments by author.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a handle on the mirror. Safe for concurrent use; writes are serialized.
func Open ¶
Open opens or creates the mirror at path and migrates it forward. A database written by a newer gadak is refused rather than used.
The data directory is created (and existing dirs tightened) to 0700; the DB file and any -wal/-shm sidecars are set to 0600. Chmod failures are logged and ignored so unsupported filesystems (or Windows) still work.
func (*DB) APIUsage ¶
APIUsage returns up to the most recent days rows, newest day first. days <= 0 means all rows.
func (*DB) APIUsageSummary ¶
func (db *DB) APIUsageSummary(ctx context.Context) (APIUsageSummary, error)
APIUsageSummary builds today + last 7 days from the mirror. Missing days are zeros. last_throttled_at on the rollup is the latest timestamp in the window.
func (*DB) AbsorbRecents ¶ added in v0.16.0
AbsorbRecents merges a localStorage dump into local.recents. Existing server rows stay in front (they are already the owner); incoming values not already present fill the remainder, still capped at RecentCap per kind. Incoming slices are newest-first, same as recentOf().
func (*DB) AddAPIUsage ¶
AddAPIUsage UPSERTs delta into the given UTC day (YYYY-MM-DD), adding to any existing counters. last_throttled_at keeps the latest non-empty timestamp.
func (*DB) AppendSyncRun ¶
AppendSyncRun stores one run and prunes the history to the newest 100.
func (*DB) AttachmentBelongs ¶ added in v0.14.1
AttachmentBelongs reports whether the mirror lists attachmentID on issueKey. The id is the Jira external id when that column is set, otherwise the store row id — the same rule handleDetail uses when it builds content URLs. One EXISTS query: serving bytes must not pay for Detail's comments/history/links/refs.
func (*DB) AttachmentOrigin ¶ added in v0.16.1
func (db *DB) AttachmentOrigin(ctx context.Context, issueKey, attachmentID string) (sourceID, contentURL string, err error)
AttachmentOrigin is the proxy's lookup: which source owns this attachment and, when stored, the origin content URL. Matches AttachmentBelongs' id rule (external_id when set, else store id).
func (*DB) CommentsByAuthor ¶
func (db *DB) CommentsByAuthor(ctx context.Context, authorID string, limit int) (CommentsByAuthorResult, error)
CommentsByAuthor returns comments for an exact author_id match, newest first. Missing author_id yields total 0 and an empty list (not an error). limit defaults to 50 and is capped at 200.
func (*DB) ComputeFieldUsage ¶
ComputeFieldUsage builds field_usage rows from issues.custom for the given aliases. total is the issue count per project; filled is how many issues have a non-empty value for that alias.
func (*DB) ConfluenceSpaceWatermarks ¶ added in v0.13.0
func (db *DB) ConfluenceSpaceWatermarks(ctx context.Context, sourceID string) (map[string]string, error)
ConfluenceSpaceWatermarks returns each space key's incremental floor for sourceID. A missing or NULL watermark is "". Callers treat empty as "not yet backfilled" and run a full fetch for that space.
func (*DB) DeleteItems ¶
DeleteItems removes items whose keys have left the source's scope and records a tombstone so `delta` can report the deletion to a client that missed it.
func (*DB) DeletedKeysSince ¶
DeletedKeysSince lists keys tombstoned after the cursor. `delta` must report these: a missed deletion leaves a tombstone visible in the client forever.
func (*DB) Detail ¶
Detail assembles one issue. An unknown key returns ErrNotFound so the handler can answer 404 without importing database/sql.
func (*DB) DistinctCount ¶
DistinctCount returns COUNT(DISTINCT col) for a fixed known table.column used by doctor. Empty/NULL values are excluded.
func (*DB) DropSourceMirror ¶ added in v0.16.1
DropSourceMirror deletes everything one source mirrored: its items (and their cascaded children), FTS rows, spaces, tombstones, and sync state — so the next sync starts full against the new origin. Used when a standalone workspace converts to connected (GDK-241 / GDK-344): a pre-namespace mirror holds `jira:N` / `confluence:N` rows the new site's upsert would silently overwrite on an id collision. The disposable cache is dropped whole rather than reconciled row by row. Watches and favorites whose keys lived in the dropped mirror go with it — kept, they would silently rebind to a new origin's issue that happens to share the key (standalone defaults to project STD; a real site can too). saved_views and local.db are untouched.
func (*DB) EnrichmentsByKind ¶
func (db *DB) EnrichmentsByKind(ctx context.Context, kind string) (map[string]json.RawMessage, error)
EnrichmentsByKind returns every payload of one kind, keyed by issue key. The list and delta responses merge it in.
func (*DB) EnrichmentsFor ¶
EnrichmentsFor returns every kind attached to one issue, for the detail view.
func (*DB) FieldUsage ¶
func (db *DB) FieldUsage(ctx context.Context) ([]FieldUsageRow, error)
FieldUsage returns every field_usage row.
func (*DB) FreshenSyncClock ¶
FreshenSyncClock stamps every sync timestamp as now. It exists for throwaway fixtures — `gadak demo` and the demo recordings — where the snapshot's real age would surface as a stale-sync warning about data that is deliberately frozen. Never call this on a mirror that syncs for real: it would hide a stalled sync.
func (*DB) GroupQueryHits ¶ added in v0.16.0
GroupQueryHits is the result of Config.GroupQuery. Keys not in the map fall through to groupRules / assignee. A present empty string means unclassified (stop).
func (*DB) HasCustomFieldKeysInRaw ¶
HasCustomFieldKeysInRaw reports whether any stored raw document contains a customfield_ key under fields. Used by `gadak fields --apply` to refuse when the mirror was never synced with custom fields.
func (*DB) HasPageVersion ¶ added in v0.16.0
HasPageVersion reports whether a stamp for this version number is already stored. Sync uses it as the incremental gate: if the incoming page version is already on disk, history cannot have grown and must not be refetched.
func (*DB) History ¶ added in v0.13.0
func (db *DB) History(ctx context.Context, opts HistoryOpts) (HistoryPage, error)
History returns visits and searches newest-first. Cursor is opaque.
func (*DB) ImportRecents ¶ added in v0.16.0
ImportRecents upserts export-file rows. File used_at wins for a (kind, value) pair; each kind is then trimmed to RecentCap newest. Local-only pairs stay if they still fit in the cap.
func (*DB) IssueLites ¶
IssueLites returns the whole mirror, which is what `bootstrap` sends.
func (*DB) IssueLitesByKeys ¶ added in v0.14.1
IssueLitesByKeys returns the IssueLite rows for keys, in the order asked. Missing and empty keys are skipped. Duplicate keys yield duplicate rows. The SQL IN-list is de-duplicated; callers that need FTS rank pass the ranked key list and get that order back.
func (*DB) IssueLitesSince ¶
IssueLitesSince returns rows written at or after the given cursor. Only a row whose content actually changed has a newer synced_at, so an idle poll returns none. The bound is inclusive on purpose: re-sending a row the client already has is a harmless upsert, while missing one leaves it stale forever.
func (*DB) KeySource ¶ added in v0.16.1
KeySource returns the source_id owning key in the mirror, "" when the key is not mirrored, and ErrKeyAmbiguous when two sources both mint it — silently preferring one source sent writes to a tracker the screen was not showing (GDK-400).
func (*DB) KeysBySource ¶ added in v0.16.1
KeysBySource returns issue keys mirrored from one source, in key order.
func (*DB) MarkFeedRead ¶
func (db *DB) MarkFeedRead(ctx context.Context, opts MarkFeedReadOpts) (MarkFeedReadResult, error)
MarkFeedRead upserts read receipts and returns the refreshed unread counts.
func (*DB) PageDetail ¶
PageDetail assembles one page. An unknown key returns (nil, nil).
func (*DB) PageStamps ¶ added in v0.14.2
func (db *DB) PageStamps(ctx context.Context, sourceID, spaceKey string) (map[string]PageStamp, error)
PageStamps returns the version stamp of every mirrored page in one space, keyed by the source's own page id (items.external_id). It exists so an incremental sync can answer "do I already hold this page?" from the mirror instead of spending a body fetch to find out — a search hit already carries version.number and version.when.
The mirror is a disposable cache, so a missing or malformed row simply means "fetch it": callers must treat an absent key as unknown, never as unchanged.
func (*DB) PageVersions ¶ added in v0.16.0
PageVersions returns stamps for itemID in number order. Missing item is empty.
func (*DB) PoolStats ¶ added in v0.16.0
PoolStats reports the connection pool's state. A caller that has finished with a mirror can assert nothing is still checked out: a connection that outlives Close belongs to something still running, and under WAL it writes journal files back into a directory the caller may already have removed (GDK-270).
func (*DB) PruneConfluenceSpaces ¶ added in v0.13.0
func (db *DB) PruneConfluenceSpaces(ctx context.Context, sourceID string, keepKeys []string) (int, error)
PruneConfluenceSpaces deletes mirrored wiki pages (and their items, FTS rows, and cascaded children) whose space_key is not in keepKeys, then drops the matching spaces rows. keepKeys empty is a no-op so a zero-scope call cannot wipe the mirror. The returned count is the number of pages removed (space-row-only cleanup is not counted).
func (*DB) PruneLocalHistory ¶ added in v0.13.0
PruneLocalHistory deletes visit/search rows older than LocalRetention. Called from the same pass as tombstone expiry (DeleteItems).
func (*DB) PurgeIssueIDsOutsideNamespace ¶ added in v0.16.1
PurgeIssueIDsOutsideNamespace deletes one source's issue rows whose item id does not start with ns+":". Upgrade path for GDK-241: a standalone mirror written before ids were namespaced holds `jira:N` rows whose keys the next sync re-inserts as `standalone-jira:N` — same (source_id, key), new id — which UNIQUE(source_id, key) rejects. The rows re-mirror immediately under the new namespace, so no tombstones are written. Children go via ON DELETE CASCADE; items_fts is contentless and needs the explicit delete.
func (*DB) PurgePageIDsOutsideNamespace ¶ added in v0.16.1
PurgePageIDsOutsideNamespace is the wiki sibling of PurgeIssueIDsOutsideNamespace (GDK-344). A standalone page's key is its numeric external id, so a pre-namespace `confluence:N` row and the namespaced `standalone-confluence:N` insert share UNIQUE(source_id, key).
func (*DB) PutSavedView ¶
PutSavedView inserts or replaces a view. The caller owns id generation.
func (*DB) Recents ¶ added in v0.16.0
Recents returns recent-use rows newest-first. Empty kind lists every kind.
func (*DB) RecordRecent ¶ added in v0.16.0
RecordRecent puts value at the front of kind (de-dup, cap RecentCap). Empty kind or value is refused. Kind is an opaque string — same names the web helper already used (assignee, transition:<project>, create-type:<project>, …).
func (*DB) RecordSearch ¶ added in v0.13.0
func (db *DB) RecordSearch(ctx context.Context, query string, resultCount int, openedKind, openedKey string) (Search, error)
RecordSearch appends one search. openedKind/openedKey may both be empty, or both set to the item opened from this search.
func (*DB) RecordSync ¶
RecordSync stores the run's bookkeeping. It does not bump `version`: a run that changed no rows must leave the ETag alone. A successful run advances first_sync_at (once) and sync_count.
func (*DB) RecordVisit ¶ added in v0.13.0
RecordVisit appends one view. Same (kind, key) twice is two rows.
func (*DB) ReingestCustom ¶
func (db *DB) ReingestCustom(ctx context.Context, specs []fields.SpecIDs, bodyFieldIDs []string) (int, error)
ReingestCustom recomputes issues.custom and the FTS body from the stored raw JSON — no network. Returns the number of issues rewritten.
func (*DB) ReplaceFieldUsage ¶
func (db *DB) ReplaceFieldUsage(ctx context.Context, rows []FieldUsageRow) error
ReplaceFieldUsage replaces the entire field_usage table.
func (*DB) ReplacePageVersions ¶ added in v0.16.0
ReplacePageVersions writes the complete stamp list for one item. The caller must have a successful full listing; an empty slice clears stored rows. Re-running with the same numbers is a no-op on cardinality (PK upsert).
func (*DB) ReplaceSourceQueries ¶ added in v0.13.0
func (db *DB) ReplaceSourceQueries(ctx context.Context, sourceID string, queries []SourceQuery) error
ReplaceSourceQueries swaps one source's named queries in a single write. An empty list clears that source (the account deleted every filter).
func (*DB) ScanFieldFill ¶
func (db *DB) ScanFieldFill(ctx context.Context, fn func(projectKey string, fieldVals map[string]json.RawMessage) error) error
ScanFieldFill streams every issue's project key and the fields object from stored raw JSON. Rows with empty or unparseable raw are skipped (older rows). fn receives field id → raw value for custom and system fields present in raw.
func (*DB) SchemaVersion ¶
SchemaVersion is the migration level this binary applied.
func (*DB) Search ¶
Search runs a key lookup (when the query looks like a key) then an FTS5 query over titles, bodies and comment text. Key hits are reserved at the front of Keys/Pages so FTS cannot drop them when filling limit. Bare terms are rewritten as quoted prefix queries (see ftsPrefixQuery). A query FTS5 cannot parse is retried as a literal phrase rather than surfaced as an error, because this is fed raw user input. limit applies to the combined result (key hits + FTS), with key hits taking slots first.
func (*DB) SearchExplain ¶ added in v0.15.0
SearchExplain is Search plus a per-hit reason list. The FTS query is the same; only the returned Explain slice is extra.
func (*DB) SetFavorite ¶
SetFavorite adds or removes a favorite issue key.
func (*DB) SetLastNotifiedAt ¶
SetLastNotifiedAt advances the OS-notification watermark. Never touches feed_reads.
func (*DB) SetSearchOpened ¶ added in v0.13.0
SetSearchOpened stamps the item opened from an existing search row.
func (*DB) SetSpaceWatermark ¶ added in v0.13.0
SetSpaceWatermark writes one space's incremental floor. An unknown key is inserted so a just-scoped space can be stamped after its first successful pass without a separate UpsertSpaces.
func (*DB) SourceQueries ¶ added in v0.13.0
SourceQueries returns one source's named queries, starred first then by name.
func (*DB) SyncState ¶
SyncState reads the state for one source. A source that has never synced returns a zero state, not an error.
func (*DB) TableCount ¶
TableCount returns SELECT COUNT(*) for a fixed known table name used by status and doctor. Only whitelisted table names are accepted.
func (*DB) UpsertIssues ¶
UpsertIssues writes one page of sync output in a single transaction and returns how many items it actually changed. Derived fields are recomputed from the batch's changelog, never carried over (contracts/sync.md invariant 5).
An item whose updated_at is unchanged is skipped whole — children, FTS and the version counter included — unless Batch.Force is set. That is what makes an incremental re-run over the watermark overlap window a no-op.
func (*DB) UpsertPages ¶
UpsertPages writes document records in a single transaction and returns how many items it actually changed. A page whose stored body, meta and comments match the incoming record is skipped (no version bump). Comment-only edits still bump because comments are part of the compare — that is the comments- only trap the old always-rewrite path existed to close.
func (*DB) UpsertSource ¶
UpsertSource records a connector instance. Credentials never come near it (Constitution Article 8).
func (*DB) UpsertSpaces ¶
UpsertSpaces writes wiki space rows (key → name/kind/homepage_id) for a source. Empty name/kind/homepage_id on conflict keeps the previous value so page-hit upserts (name only) do not wipe kind or homepage filled by a full space listing or per-space GET.
func (*DB) WriteBusyRetries ¶ added in v0.16.0
WriteBusyRetries is how many times write() has retried SQLITE_BUSY (5 or 517) on this handle. Same shape as PoolStats: a cheap accessor, no logs.
type DeriveInput ¶
type DeriveInput struct {
Changelog []ChangeEntry
Categories map[string]string // status id -> new | inprogress | done
CurrentCategory string // the issue's category right now
Priority string
Priorities []string // site priority names, most urgent first
Comments []Comment
Links []Link
}
DeriveInput is everything the derived-field rules need. Changelog entries carry only status ids, so the id -> category map has to come from the site's status list, which the connector supplies per batch.
type Derived ¶
type Derived struct {
StatusChangedAt *string
ResolvedAt *string
ReopenCount int
ReopenedAt *string
ReopenReason string
AssigneeChangedAt *string
CommentCount int
PriorityRank int
ClonedFrom string
}
Derived holds the columns gadak computes because the source does not provide them. Rules are documented in docs/DERIVE.md, and a test there requires every field of this struct to appear in that file.
func Derive ¶
func Derive(in DeriveInput) Derived
Derive computes every derived field in one pass over the issue's changelog. A status id missing from the category map counts as not-done, which can only ever miss a reopen — never invent one.
type Detail ¶
type Detail struct {
IssueKey string `json:"issue_key"`
DescriptionADF json.RawMessage `json:"description_adf"`
// DescriptionText is items.body_text. Linear (and any source that does not
// store ADF) lands markdown/plain here; surfaces fall back to it when
// DescriptionADF is empty. Never stuff markdown into DescriptionADF.
DescriptionText string `json:"description_text,omitempty"`
Comments []DetailComment `json:"comments"`
Attachments []DetailAttachment `json:"attachments"`
History []DetailChange `json:"history"`
LinkedIssues []DetailLink `json:"linked_issues"`
// RefPages are wiki pages this issue's body/comments mention (item_refs,
// target_kind=page). Only pages present in the mirror; empty omitted.
RefPages []PageLite `json:"ref_pages,omitempty"`
// BacklinkPages are wiki pages that mention this issue key. Empty omitted.
BacklinkPages []PageLite `json:"backlink_pages,omitempty"`
// Custom is the issue's full alias→value map. List rows strip body-role
// values (they can be document-sized); detail is where they surface.
Custom map[string]any `json:"-"`
}
Detail is everything the on-demand detail view needs, assembled from the mirror with no call to the source.
type DetailAttachment ¶
type DetailAttachment struct {
ID string `json:"id"`
ExternalID string `json:"external_id"`
Filename string `json:"filename"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Author string `json:"author,omitempty"`
AuthorID string `json:"author_id,omitempty"`
CreatedAt string `json:"created_at"`
URL string `json:"url,omitempty"`
}
DetailAttachment is metadata only; the handler turns ExternalID into the content proxy path. URL is the origin content URL when stored (Linear); empty for Jira.
type DetailChange ¶
type DetailChange struct {
At string `json:"at"`
Author string `json:"author"`
AuthorID string `json:"author_id,omitempty"`
Field string `json:"field"`
FromValue string `json:"from_value"`
FromID string `json:"from_id"`
ToValue string `json:"to_value"`
ToID string `json:"to_id"`
}
DetailChange is one history row. The ids come along because display values are localized: a caller that wants the `from_category` / `to_category` the detail contract allows has to resolve them from an id, never from a name.
type DetailComment ¶
type DetailComment struct {
ID string `json:"id"`
ExternalID string `json:"external_id"`
Author string `json:"author"`
AuthorID string `json:"author_id"`
BodyADF json.RawMessage `json:"body_adf"`
Body string `json:"body"` // flattened; the client's fallback when ADF will not render
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
DetailComment is one comment as the detail panel renders it.
type DetailLink ¶
type DetailLink struct {
Key string `json:"key"`
Type string `json:"type"`
Direction string `json:"direction"`
Summary string `json:"summary"`
StatusCategory string `json:"status_category"`
}
DetailLink is an edge plus whatever the mirror knows about the far side. A target outside the mirror keeps an empty summary and category.
type FeedFocus ¶
type FeedFocus string
FeedFocus is the focus tab the client sends: all | assignee | reporter | mention. "watched" is a reason, not a focus filter.
type FeedIdentity ¶
type FeedIdentity struct {
AccountID string
Email string
DisplayName string // TokenOwner / display name for author matching
}
FeedIdentity is the local user, used for relevance and self-action exclusion. AccountID comes from config (Jira /myself); Email/DisplayName fall back when the mirror still only has display names on some rows.
type FeedItem ¶
type FeedItem struct {
ID int `json:"id"`
EventID string `json:"event_id"`
IssueKey string `json:"issue_key"`
Summary string `json:"summary"`
CurrentStatus string `json:"current_status"`
EventType string `json:"event_type"`
OccurredAt *string `json:"occurred_at"`
ActorName string `json:"actor_name"`
Payload map[string]any `json:"payload"`
Reasons []string `json:"reasons"`
ReadAt *string `json:"read_at"`
}
FeedItem is one activity row the client renders (web/src/lib/types.ts FeedItem).
type FeedOpts ¶
type FeedOpts struct {
Focus FeedFocus
Limit int
Me FeedIdentity
// Now, when set, freezes the 30-day window for tests.
Now time.Time
}
FeedOpts configures a personal-feed query.
type FeedResult ¶
type FeedResult struct {
Items []FeedItem `json:"items"`
UnreadCounts FeedUnreadCounts `json:"unread_counts"`
}
FeedResult is GET feed/ response body (without the outer wrapper keys).
type FeedUnreadCounts ¶
type FeedUnreadCounts struct {
All int `json:"all"`
Assignee int `json:"assignee"`
Reporter int `json:"reporter"`
Mention int `json:"mention"`
}
FeedUnreadCounts is the badge counts per focus tab.
type FieldUsageRow ¶
FieldUsageRow is one (project, alias) fill statistic from field_usage.
type HistoryItem ¶ added in v0.13.0
type HistoryItem struct {
Type string `json:"type"` // visit | search
ID int64 `json:"id"`
Kind string `json:"kind,omitempty"`
Key string `json:"key,omitempty"`
Query string `json:"query,omitempty"`
ResultCount *int `json:"result_count,omitempty"`
OpenedKind *string `json:"opened_kind,omitempty"`
OpenedKey *string `json:"opened_key,omitempty"`
At string `json:"at"`
}
HistoryItem is one row of the mixed timeline (newest first).
type HistoryOpts ¶ added in v0.13.0
type HistoryOpts struct {
// Kind is "issue", "page", or "search". Empty means all three.
Kind string
Limit int
Cursor string
}
HistoryOpts filters the mixed visit+search timeline.
type HistoryPage ¶ added in v0.13.0
type HistoryPage struct {
Items []HistoryItem `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
}
HistoryPage is one cursor page of History.
type Issue ¶
type Issue struct {
ProjectKey string
IssueType string
IssueTypeID string
Status string
StatusID string
StatusCategory string // new | inprogress | done
Priority string
PriorityID string
Assignee string
AssigneeID string
AssigneeEmail string
Reporter string
ReporterID string
ReporterEmail string
ParentKey string
// HierarchyLevel is the source-neutral tree rank of this issue's type
// (e.g. epic=1, standard=0, sub-task=-1). Used to derive EpicKey.
HierarchyLevel int
Labels []string
Components []string
FixVersions []string
AffectsVersions []string
EnvironmentText string
Duedate string
Resolution string
DescriptionADF json.RawMessage
Custom map[string]any // mapped custom fields, keyed by config alias
Raw json.RawMessage
}
Issue is the tracker projection. `item_id`, `key`, `created_at`, `updated_at` are taken from the record's Item and every derived field is computed by the store, so a connector fills none of them.
type IssueLite ¶
type IssueLite struct {
IssueKey string `json:"issue_key"`
Summary string `json:"summary"`
ProjectKey string `json:"project_key"`
IssueType string `json:"issue_type"`
IssueTypeID string `json:"issue_type_id"`
Status string `json:"status"`
StatusID string `json:"status_id"`
StatusCategory string `json:"status_category"`
Priority *string `json:"priority"`
// PriorityID is the stable Jira priority id (issues.priority_id). Empty
// on rows a sync has not rewritten since the column was added — clients
// fall back to the display name for those, the same way they do for a
// missing status_id / issue_type_id.
PriorityID string `json:"priority_id"`
PriorityRank int `json:"priority_rank"`
Assignee *string `json:"assignee"`
AssigneeID *string `json:"assignee_id"`
AssigneeEmail *string `json:"assignee_email"`
Reporter *string `json:"reporter"`
ReporterID *string `json:"reporter_id"`
ReporterEmail *string `json:"reporter_email"`
// EpicKey is the nearest hierarchy_level==1 ancestor (derived). Nil when
// none. Distinct from ParentKey, which is the direct parent only.
EpicKey *string `json:"epic_key"`
// ParentKey is the direct parent issue key (source field), not the epic.
ParentKey *string `json:"parent_key"`
Labels []string `json:"labels"`
Components []string `json:"components"`
FixVersions []string `json:"fix_versions"`
Duedate *string `json:"duedate"`
Resolution *string `json:"resolution"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
StatusChangedAt *string `json:"status_changed_at"`
ResolvedAt *string `json:"resolved_at"`
ReopenCount int `json:"reopen_count"`
ReopenedAt *string `json:"reopened_at"`
ReopenReason *string `json:"reopen_reason"`
ClonedFrom *string `json:"cloned_from"`
// SourceProject is ClonedFrom's project prefix, precomputed because the
// list filter groups by it.
SourceProject *string `json:"source_project"`
CommentCount int `json:"comment_count"`
// Custom holds the configured field aliases from issues.custom. The server
// spreads them into the response as top-level keys, which is where the client
// reads severity and friends.
Custom map[string]any `json:"custom,omitempty"`
// Source is items.source_id (jira / linear / …). Write pickers key on it
// so a Linear row does not consume a Jira catalog or credential.
Source string `json:"source,omitempty"`
}
IssueLite is the row shape the read API hydrates the client with. Field names are the ones web/src/lib/types.ts already parses (contracts/api.md, "IssueLite"): adding is safe, renaming is not.
type IssueRecord ¶
type IssueRecord struct {
Item Item
Issue Issue
Comments []Comment
Attachments []Attachment
Changelog []ChangeEntry
Links []Link
}
IssueRecord is one item and everything hanging off it. Child lists are replaced wholesale on upsert, so a partial list would delete rows.
type Item ¶
type Item struct {
ID string // "<source_id>:<external_id>"
SourceID string
Kind string // "issue" in v0.1
ExternalID string
Key string // human-facing key, unique per source
Title string
BodyText string // flattened body, what FTS indexes
Author string
AuthorID string
URL string
CreatedAt string
UpdatedAt string
}
Item is the neutral spine row: anything with a title, body, author and timestamp fits it.
type ItemRef ¶
type ItemRef struct {
TargetKind string // "issue" | "page"
TargetKey string // issue key or page id (numeric string)
Via string // "url" | "text"
}
ItemRef is one outgoing reference from an item. Pure extraction returns these; the write path persists them into item_refs.
func ExtractIssueRefsFromPage ¶
ExtractIssueRefsFromPage finds issue keys in a page's ADF (URL paths) and plain body_text (bare keys filtered by knownProjects). Same key from both sources yields one row with via=url. knownProjects may be empty (no text hits).
func ExtractPageRefsFromIssue ¶
ExtractPageRefsFromIssue finds Confluence page IDs in an issue description (raw ADF + flattened text) and its comment bodies. Scanning ADF is what picks up link marks and inlineCards that PlainText drops — the same raw scan ExtractIssueRefsFromPage already does in the other direction. Both URL shapes use via=url. Duplicates collapse to one.
type MarkFeedReadOpts ¶
type MarkFeedReadOpts struct {
EventIDs []string
IssueKeys []string
All bool
Me FeedIdentity
Now time.Time
}
MarkFeedReadOpts is POST feed/read/ body semantics.
type MarkFeedReadResult ¶
type MarkFeedReadResult struct {
Updated int `json:"updated"`
UnreadCounts FeedUnreadCounts `json:"unread_counts"`
}
MarkFeedReadResult is POST feed/read/ response.
type Page ¶
type Page struct {
SpaceKey string
ParentID string
Version int
Status string
// Labels are source-neutral tag names (JSON array column). Empty slice is
// stored as "[]", never NULL. Sync sorts alphabetically for determinism.
Labels []string
// BodyADF is the raw Atlas Document Format body for rendering. Empty when
// the mirror only has flattened body_text (pre-v10 rows).
BodyADF json.RawMessage
}
Page is the document projection (one row in the pages table). Field names match the projection columns, not any source API (same status as Issue).
type PageComment ¶
type PageComment struct {
Author string `json:"author"`
CreatedAt string `json:"created_at"`
BodyADF json.RawMessage `json:"body_adf"`
BodyText string `json:"body_text"`
}
PageComment is one comment on a page detail response.
type PageDetail ¶
type PageDetail struct {
PageLite
BodyADF json.RawMessage `json:"body_adf"`
Comments []PageComment `json:"comments"`
// RefIssueKeys are issue keys this page's body mentions (item_refs). Only
// issues present in the mirror, sorted ascending. Empty omitted.
RefIssueKeys []string `json:"ref_issue_keys,omitempty"`
// BacklinkIssueKeys are issue keys that mention this page. Empty omitted.
BacklinkIssueKeys []string `json:"backlink_issue_keys,omitempty"`
}
PageDetail is PageLite plus the raw ADF body and comments.
type PageLite ¶
type PageLite struct {
Key string `json:"key"`
Title string `json:"title"`
SpaceKey string `json:"space_key"`
// SpaceName is the human-readable space title from the spaces table join.
// Empty when no spaces row is mirrored for this key (never omitted in JSON).
SpaceName string `json:"space_name"`
// SpaceHomepageID is the content id of the space root page (spaces.homepage_id).
// Empty when the space row is missing or homepage has not been learned yet.
SpaceHomepageID string `json:"space_homepage_id"`
ParentID string `json:"parent_id"`
Author string `json:"author"`
// AuthorID is items.author_id. Empty on legacy rows that only have a
// display name — clients group by this when present and fall back to Author.
AuthorID string `json:"author_id"`
UpdatedAt string `json:"updated_at"`
Version int `json:"version"`
URL string `json:"url"`
// Excerpt is a one-line body preview derived from body_adf (schema v15):
// whitespace-normalized, at most 200 runes. Empty when the body is empty.
Excerpt string `json:"excerpt"`
Labels []string `json:"labels"`
}
PageLite is the list/search row for a mirrored wiki page. Field names are snake_case JSON to match IssueLite and the read API contract.
type PageRecord ¶
PageRecord is one document item plus its projection and comments. Comments are replaced wholesale on upsert, matching IssueRecord.
type PageStamp ¶ added in v0.14.2
PageStamp is the mirror's record of one page's upstream identity: the source's version number and the lastModified the row was written from. Both together, never the number alone — a rollback can reuse a number.
type PageVersion ¶ added in v0.16.0
type PageVersion struct {
Number int
CreatedAt string
AuthorID string
AuthorName string
Message string
MinorEdit bool
}
PageVersion is one history stamp on a wiki page. Bodies are never stored — only the number, when, who, the editor's note, and the minor-edit flag. Field names match the page_versions columns, not the Confluence payload.
type Recent ¶ added in v0.16.0
type Recent struct {
Kind string `json:"kind"`
Value string `json:"value"`
UsedAt string `json:"used_at"`
}
Recent is one (kind, value) pair in local.recents. Unlike visits, the same pair is one row: recording it again only moves used_at to now.
type SavedView ¶
type SavedView struct {
ID string `json:"id"`
Name string `json:"name"`
Config json.RawMessage `json:"config"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
SavedView is a user's stored filter set. Personal state is the only thing in this database a user would miss, so it is also the only thing `gadak export` has to dump (Constitution Article 1).
type Search ¶ added in v0.13.0
type Search struct {
ID int64 `json:"id"`
Query string `json:"query"`
SearchedAt string `json:"searched_at"`
ResultCount int `json:"result_count"`
OpenedKind *string `json:"opened_kind"`
OpenedKey *string `json:"opened_key"`
}
Search is one append-only search execution. OpenedKind/OpenedKey name the item opened from that search, when one was.
type SearchExplain ¶ added in v0.15.0
type SearchExplain struct {
Key string `json:"key"`
Reason string `json:"reason"`
Field string `json:"field,omitempty"`
Score *float64 `json:"score,omitempty"`
}
SearchExplain is why one returned hit sits where it does. Reason is "key-exact", "key-prefix", or "fts". Field and Score are set only for fts (winning column and bm25). Filled only by SearchExplain; Search leaves Explain nil so the normal path does not allocate it.
type SearchMatch ¶
SearchMatch says which FTS column matched and shows a plain-text snippet. Field is "title" | "body" | "comment". Snippet has no HTML or highlight markers — the client highlights against its own query string.
type SearchResult ¶
type SearchResult struct {
Keys []string `json:"keys"`
Pages []PageLite `json:"pages"`
Total int `json:"total"`
Matches map[string]SearchMatch `json:"matches"`
Explain []SearchExplain `json:"explain,omitempty"`
// ElapsedMS is the searchAll wall time in milliseconds. Search leaves it
// 0 (omitted from JSON); SearchExplain fills it so --explain can name
// the query cost without changing the default Search contract.
ElapsedMS float64 `json:"elapsed_ms,omitempty"`
// contains filtered or unexported fields
}
SearchResult is a kind-aware hit list. Keys are issue keys (key-lookup hits first, then FTS among issues); Pages are page hits in the same window. Total is the number of hits returned (keys + pages), matching the pre-R2 meaning of total = len(results after limit). Matches maps each returned issue or page key to the winning FTS column match when FTS contributed one.
type SourceQuery ¶ added in v0.13.0
type SourceQuery struct {
ID string `json:"id"`
SourceID string `json:"source_id"`
ExternalID string `json:"external_id"`
Name string `json:"name"`
QueryText string `json:"query_text"`
Config json.RawMessage `json:"config"`
Favourite bool `json:"favourite"`
Owner string `json:"owner"`
Applied []string `json:"applied"`
Unsupported []string `json:"unsupported"`
UpdatedAt string `json:"updated_at"`
}
SourceQuery is a named query mirrored from a connector (Jira saved filter). Jira is the record: ReplaceSourceQueries rewrites one source's rows.
type SpaceRow ¶
SpaceRow is one wiki space (key + human name + kind). Source-neutral: a connector maps its space listing or page-embedded space ref onto this. Kind is the source type string (e.g. "global", "personal"); empty is allowed when only a name is known from a page hit. HomepageID is the content id of the space root page when known; empty on page-hit upserts so they do not wipe a value filled by a space listing.
type SyncResult ¶
type SyncResult struct {
Watermark string // ignored when empty or not greater than the stored one
FullSync bool // stamps last_full_sync_at
Err error // recorded as last_error; nil clears it
}
SyncResult is what a finished sync run reports.
type SyncRun ¶
type SyncRun struct {
Kind string `json:"kind"` // full | incremental (+reconcile)
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
Fetched int `json:"fetched"`
Changed int `json:"changed"`
Deleted int `json:"deleted"`
Error string `json:"error,omitempty"`
}
SyncRun is one recorded sync pass. Only meaningful runs are stored: ones that changed something, were a full pass, or failed — the watch loop's no-op incrementals would otherwise bury the history in noise.
type SyncState ¶
type SyncState struct {
SourceID string `json:"source_id"`
Watermark string `json:"watermark"`
// SyncedAt is the last run that finished without an error — the only field
// here that means "the mirror is fresh". A watermark stalls on its own
// whenever the project is simply quiet.
SyncedAt *string `json:"synced_at"`
Version int64 `json:"version"`
LastFullSyncAt *string `json:"last_full_sync_at"`
LastError *string `json:"last_error"`
SchemaVersion int `json:"schema_version"`
// FirstSyncAt is the first successful sync for this source (retention).
FirstSyncAt *string `json:"first_sync_at,omitempty"`
// SyncCount is the number of successful sync runs (retention).
SyncCount int64 `json:"sync_count"`
// LastNotifiedAt is the OS-notification watermark. Independent of
// feed_reads: delivering a desktop alert must not mark the feed read.
LastNotifiedAt *string `json:"last_notified_at,omitempty"`
}
SyncState is the per-source sync bookkeeping.