store

package
v0.18.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

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

View Source
const (
	FeedWindowDays   = 30
	FeedDefaultLimit = 80
	FeedMaxLimit     = 200
)

Feed window and default page size match the client contract (web/src/lib/api.ts getFeed / FeedFocus).

View Source
const (
	VisitKindIssue = "issue"
	VisitKindPage  = "page"
)

VisitKind is visits.kind / searches.opened_kind: "issue" or "page".

View Source
const (
	CommentsByAuthorDefaultLimit = 50
	CommentsByAuthorMaxLimit     = 200
)

People-axis comment list defaults (GET people/{author_id}/comments/).

View Source
const BusyHolderHint = "another gadak process (app/serve/CLI) holds this profile's mirror; close it or retry"

BusyHolderHint is the user-facing clause appended to SQLITE_BUSY errors. One owner (GDK-754 / GDK-740): sync's death path and the mirror-stale re-read warning both go through WithBusyHint, so the sentence cannot drift. It does not scan other processes — doctor lists holders separately.

View Source
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").

View Source
const CategoryInProgress = "inprogress"

CategoryInProgress pairs with CategoryDone for the lifecycle spans. Like every rule here it keys on the category, never a status name.

View Source
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.

View Source
const RecentCap = 10

RecentCap is the per-kind ceiling. Matches web/src/lib/recency.ts MAX.

Variables

View Source
var ErrInvalidCursor = errors.New("invalid cursor")

ErrInvalidCursor means a pagination cursor did not parse. The server's history handler maps it to HTTP 400 by identity — errors.Is against this value, not message text (GDK-609) — so cursor parsing must return this sentinel and never a fresh errors.New with a similar sentence.

View Source
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.

View Source
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 AliasIssueKey added in v0.17.0

func AliasIssueKey(m map[string]any)

AliasIssueKey copies m["issue_key"] onto m["key"] so map-built JSON cannot emit one name without the other (GDK-255).

func EnsureLocal added in v0.13.0

func EnsureLocal(mirrorPath string) error

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 FTSCJKBigramColumn added in v0.17.0

func FTSCJKBigramColumn(title, body, comments string) string

FTSCJKBigramColumn is the items_fts.cjk_bigram value for one row: the CJK bigrams of the title, body and comment text, space-joined. Exported because internal/snapshot writes rows through the same shape — any writer that fills only the three scored columns leaves CJK mid-match silently empty rather than broken, which is this design's named trap (0009 §Consequences).

func FormatDuration added in v0.17.0

func FormatDuration(d time.Duration) string

FormatDuration renders a span with its single largest unit — the scale the durations line reads at. Sub-minute spans keep seconds.

func IsBusy added in v0.17.1

func IsBusy(err error) bool

IsBusy reports whether err is SQLITE_BUSY (5) or SQLITE_BUSY_SNAPSHOT (517). Match on the driver's Code(), never on prose (store.go sqliteBusy).

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.

func LocalPath added in v0.13.0

func LocalPath(mirrorPath string) string

LocalPath is local.db beside the mirror at mirrorPath.

func MarshalWithIssueKeyAlias added in v0.17.0

func MarshalWithIssueKeyAlias(issueKey string, v any) ([]byte, error)

MarshalWithIssueKeyAlias encodes v then sets `"key"` equal to issueKey. Callers pass a named type alias of themselves so this is not recursive. Map-built JSON uses AliasIssueKey.

func Now

func Now() string

Now returns UTC now in config.ISOMilli. The server hands this value to clients as the `delta` cursor; see that constant for why milliseconds are required.

func OpenReadOnly added in v0.13.0

func OpenReadOnly(path string) (*sql.DB, error)

OpenReadOnly opens the mirror with SQLite mode=ro and ATTACHes local.db (created empty if missing so SELECT local.visits never fails the connection).

func ValidateRecipeName added in v0.17.0

func ValidateRecipeName(name string) error

ValidateRecipeName is the recipes save NAME rule. Empty and anything outside recipeNameRe are refused.

func WithBusyHint added in v0.17.1

func WithBusyHint(err error) error

WithBusyHint wraps a SQLITE_BUSY error so Error() names the likely holder. Non-busy errors and nil pass through. Already-hinted values are unchanged.

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 ActorPerson added in v0.17.1

type ActorPerson struct {
	AssigneeName  string
	AssigneeEmail string
	AssigneeID    string
	ReporterName  string
	ReporterEmail string
	ReporterID    string
}

ActorPerson is the assignee/reporter triple JQL people resolution needs — the narrow projection PeopleFromIssues consumes, without IssueLite's twenty-odd columns (GDK-748: loading full lites made every --jql path pay a whole-mirror row scan).

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
	// VisibilityType/VisibilityValue are the origin restriction (Jira
	// visibility.type/value). Empty means unrestricted. Linear and wiki
	// comments have no such field and stay empty.
	VisibilityType  string
	VisibilityValue string
	// JsdPublic is JSM's jsdPublic marker. nil means the origin omitted
	// the key (not a JSM project, or Jira did not send it); false is an
	// internal comment. Absence and false are distinct.
	JsdPublic *bool
}

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

func Open(path string) (*DB, error)

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 owner-writable dirs tightened) to 0700; an owner-locked directory (0555) is left locked. 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

func (db *DB) APIUsage(ctx context.Context, days int) ([]APIUsageDay, error)

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) AbsorbDashboards added in v0.17.2

func (db *DB) AbsorbDashboards(ctx context.Context, incoming []Dashboard) error

AbsorbDashboards merges incoming rows (an export file, another machine) into local.dashboards — AbsorbViews, for dashboards. Same rules: a name the server already owns wins (incoming row dropped), an id that collides gets a fresh one so an insert can never overwrite a stored row.

func (*DB) AbsorbRecents added in v0.16.0

func (db *DB) AbsorbRecents(ctx context.Context, kinds map[string][]string) error

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) AbsorbViews added in v0.17.0

func (db *DB) AbsorbViews(ctx context.Context, incoming []SavedView) error

AbsorbViews merges browser-local (localStorage) views into local.saved_views — the GDK-437 promotion day must not look like data loss. Same rules as AbsorbRecents: the server is already the owner, so an incoming view whose name exists there is dropped (server row wins), the rest are inserted. Reads order by name, so "server rows in front" reduces to that conflict rule. Kept ids stay stable — the client hides its absorbed rows by id — and an id that already exists gets a fresh one so an insert can never overwrite a server row (PutSavedView's upsert is deliberately not used here).

func (*DB) AddAPIUsage

func (db *DB) AddAPIUsage(ctx context.Context, day string, u APIUsageDelta) error

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

func (db *DB) AppendSyncRun(ctx context.Context, sourceID string, r SyncRun) error

AppendSyncRun stores one run and prunes the history to the newest 100.

func (*DB) AttachmentBelongs added in v0.14.1

func (db *DB) AttachmentBelongs(ctx context.Context, issueKey, attachmentID string) (bool, error)

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) Close

func (db *DB) Close() error

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

func (db *DB) ComputeFieldUsage(ctx context.Context, aliases []string) ([]FieldUsageRow, error)

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) Dashboard added in v0.17.2

func (db *DB) Dashboard(ctx context.Context, id string) (Dashboard, error)

Dashboard looks up one row by id. Unknown id is ErrNotFound.

func (*DB) DashboardVersion added in v0.17.2

func (db *DB) DashboardVersion(ctx context.Context) (int64, error)

DashboardVersion is the monotonic change counter for dashboard writes (0 before the first save). It moves on save and delete — the two events an open tab must reflect.

func (*DB) Dashboards added in v0.17.2

func (db *DB) Dashboards(ctx context.Context) ([]Dashboard, error)

Dashboards lists every dashboard by name.

func (*DB) DeleteDashboard added in v0.17.2

func (db *DB) DeleteDashboard(ctx context.Context, id string) error

DeleteDashboard removes one row by id and bumps the change counter. Unknown id is ErrNotFound.

func (*DB) DeleteItems

func (db *DB) DeleteItems(ctx context.Context, sourceID string, keys []string) (int, error)

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) DeleteRecipe added in v0.17.0

func (db *DB) DeleteRecipe(ctx context.Context, name string) error

DeleteRecipe removes one recipe. Unknown name is ErrNotFound.

func (*DB) DeleteSavedView

func (db *DB) DeleteSavedView(ctx context.Context, id string) error

func (*DB) DeletedKeysSince

func (db *DB) DeletedKeysSince(ctx context.Context, since string) ([]string, error)

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

func (db *DB) Detail(ctx context.Context, key string) (*Detail, error)

Detail assembles one issue. An unknown key returns ErrNotFound so the handler can answer 404 without importing database/sql.

func (*DB) DistinctCount

func (db *DB) DistinctCount(ctx context.Context, table, column string) (int, error)

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

func (db *DB) DropSourceMirror(ctx context.Context, sourceID string) error

DropSourceMirror deletes everything one source mirrored: its items (and their cascaded children), FTS rows, spaces, tombstones, sync state and sync runs — so the next sync starts full against the new origin. Used when a standalone workspace converts to connected: 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, and the personal rows keyed into it go with it.

This is the per-source half. ResetForNewOrigin is the whole conversion and is what the surfaces call; this stays exported for a single-source drop.

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

func (db *DB) EnrichmentsFor(ctx context.Context, key string) (map[string]json.RawMessage, error)

EnrichmentsFor returns every kind attached to one issue, for the detail view.

func (*DB) ExternalID added in v0.17.0

func (db *DB) ExternalID(ctx context.Context, key string) (string, error)

ExternalID is the origin's own id for a mirrored issue key — what Jira's dev-status calls issueId (numeric on Cloud, issuetap's id standalone).

func (*DB) Favorites

func (db *DB) Favorites(ctx context.Context) ([]string, error)

func (*DB) Feed

func (db *DB) Feed(ctx context.Context, opts FeedOpts) (FeedResult, error)

Feed computes personal-feed events from the mirror at query time.

func (*DB) FieldUsage

func (db *DB) FieldUsage(ctx context.Context) ([]FieldUsageRow, error)

FieldUsage returns every field_usage row.

func (*DB) FreshenSyncClock

func (db *DB) FreshenSyncClock(ctx context.Context) error

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

func (db *DB) GroupQueryHits(ctx context.Context, query string) (map[string]string, error)

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

func (db *DB) HasCustomFieldKeysInRaw(ctx context.Context) (bool, error)

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. The substring probe runs in SQL and stops at the first hit — the previous Go-side parse of every raw document cost ~1.5 s of JSON unmarshal at 20k issues (measured 2026-08-23, GDK-749). On a corpus with no custom fields the probe still walks every document (~12 KB each), which is the price of an exact no.

func (*DB) HasCustomFieldKeysInRawSampled added in v0.17.1

func (db *DB) HasCustomFieldKeysInRawSampled(ctx context.Context, limit int) (bool, error)

HasCustomFieldKeysInRawSampled is the doctor-hint twin of the probe above: it looks at at most limit documents and reports whether any carried a customfield_ key. A miss is not proof of absence — the hint it feeds says "none seen", not "none exist" — but it keeps `gadak doctor` off the whole-mirror path (GDK-749: the exact probe measured 600-860 ms on a custom-field-free 20k mirror; fields --apply keeps the exact one).

func (*DB) HasPageVersion added in v0.16.0

func (db *DB) HasPageVersion(ctx context.Context, itemID string, number int) (bool, error)

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) HasSource

func (db *DB) HasSource(ctx context.Context, id string) (bool, error)

HasSource reports whether a sources row exists for id.

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

func (db *DB) ImportRecents(ctx context.Context, items []Recent) error

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) IssueCommentCount added in v0.17.0

func (db *DB) IssueCommentCount(ctx context.Context) (int, error)

IssueCommentCount returns SUM(issues.comment_count): the comment figure the settings runtime shows, which counts issue comments only. It is deliberately not TableCount(ctx, "comments") — page comments share the comments table (upsertPageRecord rewrites them through the same insertComment helper), so the raw table count would mix wiki comments into an issue figure and move the number the settings UI has always shown (GDK-610).

func (*DB) IssueLites

func (db *DB) IssueLites(ctx context.Context) ([]IssueLite, error)

IssueLites returns the whole mirror, which is what `bootstrap` sends.

func (*DB) IssueLitesByKeys added in v0.14.1

func (db *DB) IssueLitesByKeys(ctx context.Context, keys []string) ([]IssueLite, error)

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

func (db *DB) IssueLitesSince(ctx context.Context, since string) ([]IssueLite, error)

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) IssueSyncState added in v0.17.0

func (db *DB) IssueSyncState(ctx context.Context) (SyncState, error)

IssueSyncState is the issue-origin freshness row status --json and gadak_status publish as top-level watermark / sync_count / last_error. Jira wins when it has activity so dual-source workspaces keep the historical shape; Linear wins when it is the only issue source that has run.

func (*DB) KeySource added in v0.16.1

func (db *DB) KeySource(ctx context.Context, key string) (string, error)

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

func (db *DB) KeysBySource(ctx context.Context, sourceID string) ([]string, error)

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

func (db *DB) PageDetail(ctx context.Context, key string) (*PageDetail, error)

PageDetail assembles one page. An unknown key returns (nil, nil).

func (*DB) PageLites

func (db *DB) PageLites(ctx context.Context) ([]PageLite, error)

PageLites returns every mirrored page, ordered by space then title.

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

func (db *DB) PageVersions(ctx context.Context, itemID string) ([]PageVersion, error)

PageVersions returns stamps for itemID in number order. Missing item is empty.

func (*DB) PoolStats added in v0.16.0

func (db *DB) PoolStats() sql.DBStats

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) ProjectSource added in v0.17.0

func (db *DB) ProjectSource(ctx context.Context, projectKey string) (string, error)

ProjectSource is KeySource for a project key: the source_id owning issues.project_key in the mirror. Create has no issue key yet, so routing a Linear-team create uses this. Empty when the project is not mirrored; ErrKeyAmbiguous when Jira and Linear both mint it.

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

func (db *DB) PruneLocalHistory(ctx context.Context) error

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

func (db *DB) PurgeIssueIDsOutsideNamespace(ctx context.Context, sourceID, ns string) (int, error)

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

func (db *DB) PurgePageIDsOutsideNamespace(ctx context.Context, sourceID, ns string) (int, error)

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) PutRecipe added in v0.17.0

func (db *DB) PutRecipe(ctx context.Context, name, query string) (Recipe, error)

PutRecipe inserts or overwrites a recipe. Same name keeps created_at and refreshes sql / updated_at. Name and SQL are validated here so every writer shares the rule.

func (*DB) PutSavedView

func (db *DB) PutSavedView(ctx context.Context, v SavedView) error

PutSavedView inserts or replaces a view. The caller owns id generation.

func (*DB) Query added in v0.17.0

func (db *DB) Query(query string, args ...any) (*sql.Rows, error)

Query runs a multi-row read on the same held connection (GDK-635 parent hierarchy hint). Same rule as QueryRow: do not open a second handle.

func (*DB) QueryActorPeople added in v0.17.1

func (db *DB) QueryActorPeople(ctx context.Context) ([]ActorPerson, error)

QueryActorPeople streams the assignee/reporter columns across all issues. It reads six narrow columns instead of the full IssueLite row set; the JQL resolver deduplicates in memory (people count ≪ issue count).

func (*DB) QueryIssueActors added in v0.17.0

func (db *DB) QueryIssueActors(ctx context.Context) ([]IssueActor, error)

QueryIssueActors returns every (issue, actor) touch, unordered. The set is small — bounded by comments + changelog + dev_links rows — and the caller (buildView) only folds it into a map, so no LIMIT.

func (*DB) QueryRow added in v0.17.0

func (db *DB) QueryRow(query string, args ...any) *sql.Row

QueryRow runs a single-row read on the mirror. It exists so a caller that already holds this connection (the CLI's staleness warning, GDK-314) does not open a second one — writes still go through the store API only.

func (*DB) ReadOnly added in v0.17.2

func (db *DB) ReadOnly() (*sql.DB, error)

ReadOnly opens this DB's mirror with OpenReadOnly — mode=ro, local.db attached — for executing untrusted SQL (dashboard datasources). The single purpose is that a datasource statement cannot take the mirror's write lock or mutate a row no matter what the config contains: the "writes pass through origin" invariant's integrity axis for arbitrary SQL.

func (*DB) RecentVisits added in v0.17.0

func (db *DB) RecentVisits(ctx context.Context, limit int) ([]RecentVisit, error)

RecentVisits lists the distinct (kind, key) pairs newest-first, at most limit: docs/MIRROR.md's "recently viewed" recipe (MAX(viewed_at) GROUP BY key) as a typed accessor, both kinds at once. It carries History's epoch clause for the same reason History does: after a workspace changes origin, a retired pair names a key the new origin can mint (GDK-418), and this list exists precisely so its reader goes on to open those keys.

func (*DB) Recents added in v0.16.0

func (db *DB) Recents(ctx context.Context, kind string) ([]Recent, error)

Recents returns recent-use rows newest-first. Empty kind lists every kind.

func (*DB) Recipe added in v0.17.0

func (db *DB) Recipe(ctx context.Context, name string) (Recipe, error)

Recipe looks up one named recipe. Unknown name is ErrNotFound.

func (*DB) RecipeNames added in v0.17.0

func (db *DB) RecipeNames(ctx context.Context) ([]string, error)

RecipeNames is the saved-name list a missing-name error quotes.

func (*DB) Recipes added in v0.17.0

func (db *DB) Recipes(ctx context.Context) ([]Recipe, error)

Recipes lists every recipe, newest update first.

func (*DB) RecomputeEpicKeys added in v0.17.1

func (db *DB) RecomputeEpicKeys(ctx context.Context) error

RecomputeEpicKeys rewrites issues.epic_key for the whole table. Full sync calls this once after every page has been upserted; per-batch upserts only recompute the batch and its parent-chain dependents (GDK-755).

func (*DB) RecordRecent added in v0.16.0

func (db *DB) RecordRecent(ctx context.Context, kind, value string) (Recent, error)

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

func (db *DB) RecordSync(ctx context.Context, sourceID string, r SyncResult) error

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

func (db *DB) RecordVisit(ctx context.Context, kind, key string) (Visit, error)

RecordVisit appends one view. Same (kind, key) twice is two rows. The insert uses a dedicated local.db connection so a writer holding the mirror (BEGIN IMMEDIATE / _txlock=immediate) does not block recording (GDK-753). Reads that join visits to the mirror stay on the ATTACH path.

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 *DB) ReplaceDevLinks(ctx context.Context, key string, update DevLinksUpdate) error

ReplaceDevLinks swaps one issue's dev_links rows for a successful origin answer — the mirror-refresh half of a dev-link write-through (GDK-497). Never a source of truth: the same rows are rebuilt by any later sync. update is a complete pull-request answer (empty Links drains the PR rows — GDK-580); deployment/build rows are not part of a dev-status answer and survive, written only by `gadak dev deploy`/`dev build` (GDK-592).

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

func (db *DB) ReplacePageVersions(ctx context.Context, itemID string, vers []PageVersion) error

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) ReplaceProjectVersions added in v0.17.0

func (db *DB) ReplaceProjectVersions(ctx context.Context, projectKey string, rows []VersionRow) error

ReplaceProjectVersions upserts one project's version catalog and deletes rows for that project whose id is no longer in the catalog. The catalog is the origin; this table is a cache (GDK-532). An empty list clears the project. Rows with an empty id are skipped (no join key).

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) ResetForNewOrigin added in v0.17.0

func (db *DB) ResetForNewOrigin(ctx context.Context, sourceIDs []string) (OriginReset, error)

ResetForNewOrigin is the one owner of "this workspace's origin is being replaced". Both conversion surfaces reach it through originbind.DropStandaloneProjection, so the decision cannot exist on the CLI path and not the HTTP one (GDK-247).

One transaction: a conversion that dropped the mirror but left the feed marks behind would be the same class of half-state this function exists to remove.

func (*DB) SaveDashboard added in v0.17.2

func (db *DB) SaveDashboard(ctx context.Context, name, config string) (Dashboard, error)

SaveDashboard inserts a dashboard, or updates the row that already owns name (keeping its id and created_at). The id is minted here so the CLI and the API cannot disagree about who owns id generation. Config is stored verbatim; validation of the document is internal/dashboards' job, at every writer that accepts one from a user.

func (*DB) SavedViews

func (db *DB) SavedViews(ctx context.Context) ([]SavedView, error)

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) SchemaAudit added in v0.17.0

func (db *DB) SchemaAudit(ctx context.Context) (SchemaAuditResult, error)

SchemaAudit compares the open mirror against a fresh :memory: database that ran this build's migrate() path. It issues only reads against the open file. It is a doctor entry point, not part of Open: running migrate() on :memory: on every command would be wasted work.

func (*DB) SchemaVersion

func (db *DB) SchemaVersion() int

SchemaVersion is the migration level this binary applied.

func (*DB) Search

func (db *DB) Search(ctx context.Context, query string, limit int) (SearchResult, error)

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

func (db *DB) SearchExplain(ctx context.Context, query string, limit int) (SearchResult, error)

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

func (db *DB) SetFavorite(ctx context.Context, key string, on bool) error

SetFavorite adds or removes a favorite issue key.

func (*DB) SetLastNotifiedAt

func (db *DB) SetLastNotifiedAt(ctx context.Context, sourceID, at string) error

SetLastNotifiedAt advances the OS-notification watermark. Never touches feed_reads.

func (*DB) SetSearchOpened added in v0.13.0

func (db *DB) SetSearchOpened(ctx context.Context, id int64, kind, key string) (Search, error)

SetSearchOpened stamps the item opened from an existing search row.

func (*DB) SetSpaceWatermark added in v0.13.0

func (db *DB) SetSpaceWatermark(ctx context.Context, sourceID, key, watermark string) error

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) SetWatch

func (db *DB) SetWatch(ctx context.Context, key string, on bool) error

SetWatch adds or removes a watched issue key.

func (*DB) SourceQueries added in v0.13.0

func (db *DB) SourceQueries(ctx context.Context, sourceID string) ([]SourceQuery, error)

SourceQueries returns one source's named queries, starred first then by name.

func (*DB) StatusCategories added in v0.17.0

func (db *DB) StatusCategories(ctx context.Context) (map[string]string, error)

StatusCategories is the mirror's status id -> category map: the cached origin catalog (status_catalog, written by every sync pass) overlaid on the reconstruction from issue rows that carry one — the same query loadDeriveContext runs for `issue --derive`, kept so a mirror migrated to the catalog but not yet re-synced still resolves what it can. Durations needs this to walk the changelog, which stores bare ids.

func (*DB) SyncRuns

func (db *DB) SyncRuns(ctx context.Context, sourceID string, limit int) ([]SyncRun, error)

SyncRuns returns the newest runs first, at most limit.

func (*DB) SyncState

func (db *DB) SyncState(ctx context.Context, sourceID string) (SyncState, error)

SyncState reads the state for one source. A source that has never synced returns a zero state, not an error.

func (*DB) TableCount

func (db *DB) TableCount(ctx context.Context, table string) (int, error)

TableCount returns SELECT COUNT(*) for a fixed known table name used by status and doctor. Only whitelisted table names are accepted.

func (*DB) UpsertIssues

func (db *DB) UpsertIssues(ctx context.Context, b Batch) (int, error)

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

func (db *DB) UpsertPages(ctx context.Context, records []PageRecord) (int, error)

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

func (db *DB) UpsertSource(ctx context.Context, s Source) error

UpsertSource records a connector instance. Credentials never come near it (Constitution Article 8).

func (*DB) UpsertSpaces

func (db *DB) UpsertSpaces(ctx context.Context, sourceID string, rows []SpaceRow) error

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) UserCatalog added in v0.17.0

func (db *DB) UserCatalog(ctx context.Context) ([]UserAccount, error)

UserCatalog reads the cached origin account catalog (GDK-590): every account sync has seen, with the origin's account_type spelling. Callers judge bots through the connector's one judgement function, never here — the store is source-neutral.

func (*DB) Watches

func (db *DB) Watches(ctx context.Context) ([]string, error)

func (*DB) WriteBusyRetries added in v0.16.0

func (db *DB) WriteBusyRetries() uint64

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 Dashboard added in v0.17.2

type Dashboard struct {
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Config    json.RawMessage `json:"config"`
	CreatedAt string          `json:"created_at,omitempty"`
	UpdatedAt string          `json:"updated_at,omitempty"`
}

Dashboard is one row of local.dashboards (GDK-781). Config is the {html, datasources} document internal/dashboards owns; the store keeps it as raw bytes so a newer config shape round-trips through an older binary.

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 {
	// JSON alias "key" is added by issueDoc / detailResponse / AliasIssueKey,
	// not by MarshalJSON on this type: an anonymous Marshaler embed would
	// replace the whole `gadak issue --json` object (GDK-255).
	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:"-"`
	// Created is items.created_at — the origin stamp Durations measures the
	// wait span from. Internal like Custom: the wire already carries the
	// lites' created_at, and the detail response exposes only the derived
	// spans (wait_ms / progress_ms), not this raw input.
	Created string `json:"-"`
	// DevLinks are the development-panel links (GDK-497), newest first.
	DevLinks []DevLink `json:"dev_links"`
}

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"`
	// VisibilityType/VisibilityValue are empty when the origin sent no
	// restriction. JsdPublic is nil when the jsdPublic key was absent.
	VisibilityType  string `json:"visibility_type"`
	VisibilityValue string `json:"visibility_value"`
	JsdPublic       *bool  `json:"jsd_public"`
}

DetailComment is one comment as the detail panel renders it.

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 DevLink struct {
	Kind       string // pullrequest | deployment | build (GDK-592)
	ExternalID string
	URL        string
	Title      string
	// Status is the stored form of the origin's OPEN|MERGED|DECLINED
	// vocabulary (lowercase). Produced by jira.DevPRStatus.Stored();
	// unknown origin tokens stay the lowercased payload.
	Status string
	// Author is the pull request's author (login). Actor/ActorName are who
	// wrote the link (issuetap's X-Issuetap-Actor accountId and display
	// name). Different axes — a bot links a human's PR — never merged
	// (GDK-589). Branch is the PR head ref. All ” when the origin sent
	// nothing (v33 columns are NOT NULL DEFAULT ”).
	Author    string
	Actor     string
	ActorName string
	Branch    string
	// Environment is a deployment row's target (production, staging, …)
	// — kind data with its own v36 column, never a title slot. Empty on
	// pull-request and build rows (GDK-592).
	Environment string
	UpdatedAt   string
}

DevLink is one development-panel link (GDK-497): a pull request the origin associates with the issue. URL is the idempotent key per issue.

type DevLinksUpdate added in v0.17.0

type DevLinksUpdate struct {
	Links []DevLink
}

DevLinksUpdate is a successful origin answer for one issue's development-panel links. The type exists only after a completed fetch (or a deliberate drain such as Cloud opt-out). Nil *DevLinksUpdate skips the rewrite; a non-nil value with empty Links drains. A fetch error cannot construct this value, so it cannot reach ReplaceDevLinks or the upsert rewrite (GDK-536 / GDK-580).

type DurationsInput added in v0.17.0

type DurationsInput struct {
	Created    string
	Changelog  []DetailChange
	Categories map[string]string // status id -> new | inprogress | done
	Now        time.Time
}

DurationsInput is everything the two lifecycle spans need. Changelog entries carry status ids only, so the id -> category map comes with them; Now is an input so the computation stays deterministic for tests and the server (GDK-590) can reuse it verbatim.

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.

const (
	FeedFocusAll      FeedFocus = "all"
	FeedFocusAssignee FeedFocus = "assignee"
	FeedFocusReporter FeedFocus = "reporter"
	FeedFocusMention  FeedFocus = "mention"
)

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.

func FeedIdentityOf added in v0.17.2

func FeedIdentityOf(cfg *config.Config) FeedIdentity

FeedIdentityOf maps the configured credential onto the feed identity. The sync notifier and the HTTP feed handler both build this — one owner, not two copies that can drift when the credential shape grows (GDK-820). A nil config reads as the zero identity, which the notifier's degraded path already treats as "no focus".

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).

func (FeedItem) MarshalJSON added in v0.17.0

func (f FeedItem) MarshalJSON() ([]byte, error)

MarshalJSON adds `key` as an alias of `issue_key` (GDK-255).

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

type FieldUsageRow struct {
	ProjectKey string
	Alias      string
	Filled     int
	Total      int
}

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
	// FixVersionIDs is the same-order source ids for FixVersions (column
	// fix_version_ids). Join the versions catalog on these, not on names —
	// names rename. Empty slice stores "[]", same rule as FixVersions.
	FixVersionIDs   []string
	AffectsVersions []string
	EnvironmentText string
	Duedate         string
	Resolution      string
	ResolutionID    string
	// SprintID/SprintName/SprintState are the one sprint projected from the
	// origin's sprint array (active > future > closed, then larger id). Nil /
	// empty when the site has no sprint field, the array is empty, or an
	// element was not an object. Linear leaves them unset.
	SprintID    *int64
	SprintName  string
	SprintState string
	// SecurityLevelID/SecurityLevel are the origin issue security level.
	// Empty when the origin sent no security object (unrestricted, or a
	// source that has none — Linear). Id is the key; the name is
	// display-only and localizes. nz() stores empty as NULL.
	SecurityLevelID string
	SecurityLevel   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 IssueActor added in v0.17.0

type IssueActor struct {
	IssueKey  string
	SourceID  string
	ActorID   string
	ActorName string
	Via       string // "comment" | "changelog" | "dev_link"
}

IssueActor is one touch of one issue by one account: a comment, a changelog entry, or a development-panel link (GDK-590). The union is also the issue_actors view, so `gadak sql` and documented recipes see the same axis the server builds members from.

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"`
	// HierarchyLevel is issues.hierarchy_level as stored (epic 1, standard
	// 0, sub-task −1). Projected, not recomputed.
	HierarchyLevel int      `json:"hierarchy_level"`
	Labels         []string `json:"labels"`
	Components     []string `json:"components"`
	FixVersions    []string `json:"fix_versions"`
	Duedate        *string  `json:"duedate"`
	Resolution     *string  `json:"resolution"`
	// ResolutionID is the stable Jira resolution id (issues.resolution_id).
	// Empty on rows a sync has not rewritten since the column was added —
	// the same contract as priority_id. Unresolved issues also store ”.
	ResolutionID    string  `json:"resolution_id"`
	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"`
	// SprintID/SprintName/SprintState are the projected sprint (GDK-518).
	// Nil when the origin had none, the site has no sprint field, or the
	// row predates the next sync after v30.
	SprintID    *int64  `json:"sprint_id"`
	SprintName  *string `json:"sprint_name"`
	SprintState *string `json:"sprint_state"`
	// SecurityLevelID/SecurityLevel are the origin issue security level
	// (v32). Nil when the issue is unrestricted, the origin sent no
	// security object, or the row predates the next sync after v32.
	// Id is the key (names localize); the name is display-only.
	SecurityLevelID *string `json:"security_level_id"`
	SecurityLevel   *string `json:"security_level"`
}

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.

func (IssueLite) MarshalJSON added in v0.17.0

func (l IssueLite) MarshalJSON() ([]byte, error)

MarshalJSON adds `key` as an alias of `issue_key` so JSON surfaces and SQL (`issues_full.key`) share a name (GDK-255). Derived at marshal time so a constructor cannot emit one without the other.

type IssueRecord

type IssueRecord struct {
	Item        Item
	Issue       Issue
	Comments    []Comment
	Attachments []Attachment
	Changelog   []ChangeEntry
	Links       []Link
	// Users feeds the account catalog cache. Unlike the child lists above it
	// merges rather than replaces: a row with an empty name or account_type
	// keeps what the catalog already knows (some payloads carry less than the
	// first one that mentioned the account).
	Users []UserAccount
	// DevLinks, when non-nil, is a complete origin answer (including
	// empty). Nil means the origin was not observed and existing rows
	// stay (GDK-536 / GDK-580).
	DevLinks *DevLinksUpdate
}

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

func ExtractIssueRefsFromPage(bodyADF, bodyText string, knownProjects map[string]bool) []ItemRef

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

func ExtractPageRefsFromIssue(bodyADF, bodyText string, commentBodies []string) []ItemRef

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 Link struct {
	Type      string
	Direction string // inward | outward
	TargetKey string
}

Link is an edge out of an item. TargetKey may point outside the mirror.

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 OriginReset added in v0.17.0

type OriginReset struct {
	// Removed is table → rows deleted. Only tables whose rows named the
	// retired origin appear; the mirror's own cascade is not itemised.
	Removed map[string]int `json:"removed"`
	// RetiredHistory is visit and search rows still on disk but no longer in
	// the timeline: they name keys the new origin would resolve to other
	// issues. Readable with `gadak sql` (local.visits / local.searches).
	RetiredHistory int `json:"retired_history"`
	// OriginEpoch is the generation history is stamped with from now on.
	OriginEpoch int `json:"origin_epoch"`
	// SavedViews are authored views whose stored query mentions a project the
	// retired origin owned. Kept — the user wrote them — and named here,
	// because `project = STD` now means the new site's STD. A text match on
	// the opaque config: the web owns that shape, so this over-reports rather
	// than staying silent.
	SavedViews []string `json:"saved_views_naming_retired_projects"`
}

OriginReset is what a conversion took, so a surface can say it instead of leaving the user to discover an empty feed. Keyed by table rather than by field so a rule added later reports itself without a struct change.

func (OriginReset) MarshalJSON added in v0.17.0

func (r OriginReset) MarshalJSON() ([]byte, error)

MarshalJSON keeps Removed non-nil so `--json` never emits a bare null for a field a caller iterates.

func (OriginReset) String added in v0.17.0

func (r OriginReset) String() string

String renders the reset for a human: one line, empty when nothing went.

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"`
	// BodyText is BodyADF flattened by adf.PlainText — the same walker FTS
	// indexes. Always present (empty when the body is empty) so a text client
	// does not have to parse ADF.
	BodyText string        `json:"body_text"`
	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, its plain-text form, 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

type PageRecord struct {
	Item     Item
	Page     Page
	Comments []Comment
}

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

type PageStamp struct {
	Version   int
	UpdatedAt string
}

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 RecentVisit added in v0.17.0

type RecentVisit struct {
	Kind     string `json:"kind"`
	Key      string `json:"key"`
	ViewedAt string `json:"viewed_at"`
}

RecentVisit is one (kind, key) pair folded to its newest visit — the row `gadak recents` prints. Unlike Visit there is no ID: dedup happened in SQL.

type Recipe added in v0.17.0

type Recipe struct {
	Name      string `json:"name"`
	SQL       string `json:"sql"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

Recipe is a named read-only SQL query in local.recipes (GDK-503).

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). Since GDK-105 it lives in local.db, which survives `rm gadak.db`.

type SchemaAuditResult added in v0.17.0

type SchemaAuditResult struct {
	Stamp     int      // PRAGMA user_version of the open file
	Supported int      // len(migrations) — the level this build writes
	Missing   []string // "table", "table.column", or a named index
	Extra     []string
}

SchemaAuditResult is the diagnosis of "does this file's sqlite_master match the schema this build migrates to?". Missing is the damage signal (expected tables/columns/views/indexes that are absent). Extra is informational leftover on the file.

func (SchemaAuditResult) OK added in v0.17.0

func (r SchemaAuditResult) OK() bool

OK reports whether the file is missing anything this build expects. Surplus objects do not fail the audit.

type SchemaTooNewError added in v0.17.0

type SchemaTooNewError struct {
	Path      string // the mirror file
	Have      int    // schema version found in the file
	Supported int    // highest schema version this build migrates to
}

SchemaTooNewError means the file was migrated by a gadak that reads a later schema than this build does — the app and the CLI ship as separately versioned formulae, so one build opening the mirror once is enough to lock the other out of that workspace (GDK-498).

It is a type, not a string, because more than one surface has to say the same true thing about it: nothing is lost, since the mirror is a cache the origin can rebuild. Recognise it with errors.As rather than matching text.

func (*SchemaTooNewError) Error added in v0.17.0

func (e *SchemaTooNewError) Error() string
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

type SearchMatch struct {
	Field   string `json:"field"`
	Snippet string `json:"snippet"`
}

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 Source

type Source struct {
	ID      string // stable slug, e.g. "jira"
	Kind    string
	BaseURL string
}

Source is one configured connector instance.

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

type SpaceRow struct {
	Key        string
	Name       string
	Kind       string
	HomepageID string
}

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 Spans added in v0.17.0

type Spans struct {
	Wait     *time.Duration // created -> first entry into in-progress
	Progress *time.Duration // latest entry into in-progress -> done, or Now while in progress
}

Spans holds the two lifecycle spans `gadak issue` shows. nil means the changelog cannot answer: never entered progress (Wait), or no in-progress entry to measure from (Progress).

func Durations added in v0.17.0

func Durations(in DurationsInput) Spans

Durations computes the spans from the changelog — the same walk Derive does, with two questions asked of it. Nothing is stored: data-model.md keeps time-in-status deliberately absent, so this stays a query-time computation (GDK-591).

Progress measures from the latest in-progress entry, not the first: a reopened issue that re-enters progress restarts the clock, which pairs with "still in progress means until now" — first-entry would report the whole history of a reopened issue as one uninterrupted run.

func (Spans) Line added in v0.17.0

func (d Spans) Line() string

Line is the one-line form `gadak issue` prints: "wait 3d · progress 5h". Empty when neither span exists — the caller omits the line, not its parts.

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
	// Locale records the origin locale this pass fetched display names
	// under (GDK-597). Empty leaves the stored marker alone — an error
	// path or a source that does not localize must not clobber it.
	Locale string
}

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 is the mirror's migration level (PRAGMA user_version).
	// Diagnostic surfaces (status --json, MCP gadak_status, doctor) all
	// publish this field. The sync_state.schema_version column can lag
	// when Open migrates after the last sync wrote the row (GDK-526), so
	// SyncState overwrites the scanned column with the live PRAGMA rather
	// than introducing a second JSON name for the same fact.
	SchemaVersion int `json:"schema_version"`
	// SchemaVersionRow is the stored column. json omitted so it cannot
	// collide with schema_version on the wire; doctor uses it to name a lag.
	SchemaVersionRow int `json:"-"`
	// 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"`
	// Locale is the origin locale the jira source's display names were
	// fetched under (GDK-597). NULL (pre-v35 mirror) reads as "" = English.
	Locale string `json:"locale,omitempty"`
}

SyncState is the per-source sync bookkeeping.

type UserAccount added in v0.17.0

type UserAccount struct {
	AccountID   string
	Name        string
	Email       string
	AccountType string
}

UserAccount is one row of the origin's account catalog (GDK-590). The connector collects every user payload the sync already reads — assignee, reporter, creator, comment/changelog/attachment authors — and the store caches the union in the users table. AccountType keeps the origin's spelling ("agent", "app", "atlassian", …); source-neutral on purpose, the bot judgement on those values lives in the jira package.

type VersionRow added in v0.17.0

type VersionRow struct {
	ID          string
	ProjectKey  string
	Name        string
	Released    bool
	Archived    bool
	ReleaseDate string
}

VersionRow is one row of the project version catalog (GDK-532). Id is the join key; names rename. Released/Archived are origin flags; ReleaseDate is a date-only string or empty.

type Visit added in v0.13.0

type Visit struct {
	ID       int64  `json:"id"`
	Kind     string `json:"kind"`
	Key      string `json:"key"`
	ViewedAt string `json:"viewed_at"`
}

Visit is one append-only view event.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL