cache

package
v0.18.2 Latest Latest
Warning

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

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

Documentation

Overview

Package cache implements the P7 read surface (spec docs/features/archive/v1-min-2026-07/specs/07-read-surface-statusline.md): it composes internal/fold over the committed history of every connected space's local mirror clone (internal/space) into the inbox/outbox query sets §4.2/§7.2 defines, plus per-system read cursors, the pending-merge overlay, and mirror staleness/sync-age.

ADR-001 import row: internal/artifact, internal/fold, internal/space ONLY. This package never imports internal/validate (the V5 registry-code lookup is internal/cli/cmd_show.go's job — it maps the digest/staleness FACTS this package returns to a registry code) and never imports internal/cli (P6's PendingMarker/CacheRemover interfaces live there; internal/cli/cache_wiring.go adapts this package's primitives to satisfy them, not the reverse).

This package is validate-free and mostly I/O-light: it reads mirror working-tree files directly (fast, no git subprocess) and uses exactly one `git log` invocation per connected mirror to recover first-parent commit order (D-017) — never a per-file git call. Its own on-disk cache root (cursor.json, pending markers, both under the caller's cacheDir, i.e. `.a2a/cache/`) is machine-local, gitignored and fully rebuildable (D-001): losing it only costs a "read as new" on the next inbox run, never data loss.

Every entry point takes a Clock (never calls time.Now() internally) so staleness/TTL/SLA math is deterministic under test.

Overdue: the deadline, shown to the party that can actually meet it.

`outbox --attention` has always reported `needed-by-passed` — to the SENDER. That is the person who asked for the work and whose only remaining move is to nag. The responder, the one who can close it, was told nothing: `--actionable` is OP-207's normative five-condition union and `needed_by` is not among them, and its first condition is "addressed to me with no ack by me" — so the moment you acknowledge a request it leaves your actionable list, while the work and its deadline stay entirely yours.

For a permanent record spanning two companies that is the failure that costs the most: not a wrong answer, a silence that both sides read differently. The requester sees an overdue item and infers neglect; the responder sees an empty list and infers nothing is owed.

This is a SEPARATE query rather than a sixth condition on `--actionable`, deliberately. That union is quoted verbatim from the plan (OP-207, spec 07 §T1) and is what the statusline, the MCP read surface and every consumer script already agree on; widening it is a protocol change and belongs in the surfaced-diff flow, not in the commit that noticed the gap.

Index

Constants

View Source
const (
	// SkipReasonUnreadable: the file could not be opened/read at all
	// (os.Open/io.ReadAll failure via readBounded), or filepath.WalkDir
	// itself reported a traversal error for that path (e.g. a directory it
	// could not list on the way there).
	SkipReasonUnreadable = "unreadable"

	// SkipReasonNotFrontmatterShaped: the file's bytes do not contain the
	// well-formed `---\n<yaml>\n---\n` delimiter pair at all
	// (artifact.ErrNoFrontmatter) — not an envelope document, structurally.
	// Artifact-only; walkEvents never produces this reason (event/v1 files
	// carry no frontmatter delimiters to be missing).
	SkipReasonNotFrontmatterShaped = "not-frontmatter-shaped"

	// SkipReasonUndecodableYAML: the block that should decode as YAML does
	// not. Two distinct causes collapse into this one reason, deliberately:
	// (1) artifact.ParseFrontmatter's own syntax probe rejected the
	// delimited block (artifact.ErrMalformedFrontmatter) — e.g. a duplicate
	// mapping key, the exact defect filed 2026-07-26 (a doubled `thread:`
	// line made yaml.Unmarshal reject the whole envelope); or (2) the block
	// parsed as generic YAML but does not fit this package's typed
	// envelope/event probe (decodeEnvelope/decodeEvent). Both are "the YAML
	// here doesn't decode" from a reader's point of view — splitting them
	// by which internal call happened to reject it would leak an
	// implementation detail into a reason string agents key off of.
	SkipReasonUndecodableYAML = "undecodable-yaml"

	// SkipReasonNoID: the document decoded but carries no `id` (artifact)
	// or no `event` (event) — the field every downstream computation in
	// this package requires.
	SkipReasonNoID = "no-id"

	// SkipReasonUnrelativizable: filepath.Rel(dir, path) itself failed, so
	// no space-relative path could be computed for the file at all — Path
	// carries the raw path this package saw instead (never empty).
	SkipReasonUnrelativizable = "unrelativizable-path"
)

The SkipReason* vocabulary names which stage of the best-effort decode rejected a file — fixed, short, stable strings, because stage 2 (internal/cli, internal/mcp) matches on these verbatim rather than pattern-matching a free-text error message.

View Source
const (
	// ThreadOrderCommitted is set when the ordering rests on the space's
	// per-commit first-parent sequence (D-017/D-026) — the guarantee that
	// provably matches the fold beside it (§T3).
	ThreadOrderCommitted = "committed"
	// ThreadOrderDeclared is set when commitOrder returned no history
	// (unreadable/absent git log) and the reader fell back to the
	// envelope's own `created`/event's own `at` fields — a WEAKER,
	// author-supplied ordering, surfaced rather than silently substituted
	// (§T3 "Degradation is designed, not silent").
	ThreadOrderDeclared = "declared"
)
View Source
const DefaultStalenessSLA = 7 * 24 * time.Hour

DefaultStalenessSLA is OP-208's default "no event for the space's staleness SLA" window (7 days) when space.yaml carries no override.

View Source
const DefaultStatuslineTTL = 5 * time.Minute

DefaultStatuslineTTL is §7.5's default cache-age TTL (5 minutes) that triggers a detached background refresh.

View Source
const DefaultUpdateCheckTTL = 6 * time.Hour

DefaultUpdateCheckTTL is the T3 update-check cache's default freshness window (spec 19 T3: "default 6h ... release cadence is daily-ish, not minutes") — distinct from DefaultStatuslineTTL, which governs mirror sync-age, not the machine-level release-check cache.

View Source
const ReadRefreshTTLForTest = readRefreshTTL

ReadRefreshTTLForTest exposes readRefreshTTL to guards in OTHER packages — specifically cmd/a2a's wire tier, which drives the real `a2a inbox` closure and must assert that its fixture's mirror age actually straddles this window. Following the package's existing SetCloneOrFetchForTest convention.

A cross-package test that hard-coded "30s" would keep passing after this constant moved, while silently no longer exercising the window it was written for — which is the precise vacuity that let RN-0910-1 ship past a green matrix.

Variables

View Source
var ErrNotFound = fmt.Errorf("cache: artifact not found")

ErrNotFound is returned by Show when ref does not resolve to any artifact in any connected space.

View Source
var ErrSyncBudgetExhausted = errors.New("cache: SyncIfStale: budget exhausted")

ErrSyncBudgetExhausted is wrapped into one returned error per mirror SyncIfStale did not get a chance to attempt because totalBudget ran out first.

View Source
var ErrThreadNotFound = fmt.Errorf("cache: thread not found")

ErrThreadNotFound is returned by ThreadView when threadID (whether supplied directly or resolved from a member artifact id) matches no artifact in any space ThreadView searched — D6's "an unknown id exits 0 with an empty list, indistinguishable from a real empty thread" closed for the bare thread-id case (ErrNotFound already closes it for the any-member/artifact-id case, reused below). Not spec-mandated by name; this package's own choice, consistent with every other "never a silent empty success" rule this phase enforces — see Deviations. When --space was supplied this error does NOT mean "no such thread anywhere": it may exist in a different connected space the caller did not ask about.

Functions

func CommittedEvents added in v0.13.0

func CommittedEvents(mirrorDir, system, subject string) ([]fold.Event, error)

CommittedEvents reads every committed event/v1 YAML file under mirrorDir/system/events/<year>/*.yaml and returns the subset whose `subject` equals subject, as fold.Event values (ULID/Subject/ Transition/Version/Actor only — no ClaimedState, no CommitSeq: the identical shape the two former adapter-local copies produced, now with Version added so a contract publish/deprecate/retire read through this path carries its per-version fact instead of silently dropping it). An absent events/ directory (fresh mirror, nothing committed for this system yet) returns (nil, nil), never an error.

func ConfigureUpdateNotice

func ConfigureUpdateNotice(s *Store, binaryVersion string, defaults map[string]string)

ConfigureUpdateNotice enables the proactive update notice on s from the machine config's free-form defaults map (spec 19 T3): update_repo defaults to release.DefaultUpdateRepo; update_check_ttl (a Go duration string) defaults to DefaultUpdateCheckTTL. It wires the background checker that refreshes the machine-level update-check cache (statusline fires it via triggerUpdateRefreshIfStale; the checker writes the cache ONLY — D-021). A cache-path resolution failure disables the notice quietly (advisory display is never fatal). The lead calls this at each Store construction site (CLI read store, MCP buildStore) so every Store-based surface shares one configured notice. Takes the raw defaults map, not space.MachineConfig, so internal/cache stays decoupled from internal/space's config type.

func FindRegisteredConsumers added in v0.13.0

func FindRegisteredConsumers(mirrorDir, contractID string) (map[string]bool, error)

FindRegisteredConsumers returns every registered consumer of contractID, UNSCOPED by major — this is contractDeprecateAddressees' own query (WHO a deprecation announcement is addressed to; Edge 1 scopes the RETIRE gate only, spec 04 §4, so a deprecation's addressee set is unchanged). See FindRegisteredConsumersForMajor for the version-scoped variant.

func FindRegisteredConsumersForMajor added in v0.13.0

func FindRegisteredConsumersForMajor(mirrorDir, contractID string, major int) (map[string]bool, error)

FindRegisteredConsumersForMajor is the same D-022 union, scoped to major (Edge 1): a consumes.yaml dependency counts only when its own `major` equals major; a satisfied requirement counts regardless of major (see this file's own doc comment for why).

func FormatSkippedList added in v0.13.0

func FormatSkippedList(skipped []SkippedFile) string

FormatSkippedList renders a skip report as `path (reason), path (reason)`.

It lives here rather than once per surface for the reason the wave that added it was already applying elsewhere: it operates purely on this package's own type, both surfaces already import this package, and a four-line formatter duplicated across `internal/cli` and `internal/mcp` is the same shape of drift — smaller, but the same — as the duplicated walk that wave existed to delete. The FRAMING sentence around this list stays per-surface and deliberately differs: a read verb reporting its own item list and a refusal explaining why a good ref failed must not send the reader to the same place.

func ReleaseStatuslineRefreshLease added in v0.16.3

func ReleaseStatuslineRefreshLease(cacheDir string)

ReleaseStatuslineRefreshLease lets the canonical sync command clear a lease regardless of whether sync was invoked manually or by statusline.

func RemoveMarker added in v0.15.0

func RemoveMarker(cacheDir, spaceID, artifactID string) error

RemoveMarker removes one artifact's pending marker. Absence is success.

func RemoveSpaceMarkers

func RemoveSpaceMarkers(cacheDir, spaceID string) error

RemoveSpaceMarkers deletes every pending marker recorded for spaceID — the cache-side half of `a2a disconnect`'s "remove ... cache for that space" step (§7.2 OP-202); the cursor's own item-state entries for that space's items are left as harmless orphans (self-correcting: a disconnected space's items simply stop appearing in any future index, D-001 rebuildability).

func RenderStatusline added in v0.15.0

func RenderStatusline(result StatuslineResult, prefix bool) string

RenderStatusline renders an already-computed result. prefix controls only the published leading "a2a: " presentation; update-only and quiet results remain byte-compatible with the original contract.

func SortNotificationReasons added in v0.15.0

func SortNotificationReasons(items []Item)

SortNotificationReasons makes transition fingerprints deterministic.

func WriteMarker

func WriteMarker(cacheDir, spaceID string, m PendingMarker) error

WriteMarker persists m for (spaceID, m.ArtifactID) under cacheDir.

Types

type ArtifactIndexEntry added in v0.13.0

type ArtifactIndexEntry struct {
	Path   string
	Thread string
	Digest string
}

ArtifactIndexEntry is one artifact's resolver-relevant facts, as decoded by walkArtifacts' own best-effort stage chain: the space-relative path (KnownArtifact/Digest's data source) and the `thread` frontmatter field (ThreadOf/ThreadExists' data source) — the same two facts internal/cli's and internal/mcp's own former mirrorArtifact types each carried, now sourced from ONE walk rather than two independent ones. Digest is walkArtifacts' own decodeArtifactFile-computed digest as of THIS walk — see BuildArtifactIndex's own doc comment for what that means for a caller across a long-lived resolver's lifetime.

type Clock

type Clock func() time.Time

Clock is the injected "now" source every staleness/TTL computation in this package uses instead of a buried time.Now() call (rails: "no time.Now() buried in cache logic — take a clock/now for testability").

type ContractInfo

type ContractInfo struct {
	Space    string `json:"space"`
	ID       string `json:"id"`
	Provider string `json:"provider"`
	Version  string `json:"version,omitempty"`
	State    string `json:"state"`
	// Contract-specific envelope facts are dashboard-only today. json:"-"
	// preserves the established `a2a contracts --json` shape while allowing
	// the richer HTML assembler to stop guessing whether a contract is
	// code-generated and what compatibility vocabulary it declares.
	Category      string `json:"-"`
	SchemaFormat  string `json:"-"`
	CompatPolicy  string `json:"-"`
	GeneratedTool string `json:"-"`
	SourceDigest  string `json:"-"`
	// Description is a short human-readable summary from the contract's body,
	// for the dashboard's dependency map. json:"-" keeps `a2a contracts --json`
	// byte-stable for its existing consumers (read as a Go field only).
	Description string `json:"-"`
	// Versions is the ROLLING WINDOW: every version this contract has ever
	// published, with the state it holds now, oldest first (P4,
	// agent-ops-2026-07). `Version`/`State` above are the summary — newest
	// published version, and the subject-level projection over these — and
	// they stay exactly what they were. This is the detail neither of them
	// can carry: "1.0 retired, 1.2 published, 2.0 published" is three facts,
	// and a reader shown only the projection cannot tell a contract with one
	// live version from one with three.
	//
	// `omitempty`, so a contract whose history records no version at all —
	// the shape every history had before P4 — produces byte-identical
	// `a2a contracts --json` output. For a contract that HAS versions this
	// is a new field in that output, which is additive and deliberate: a
	// consumer that could not see the window could not act on it.
	Versions []ContractVersion `json:"versions,omitempty"`
}

ContractInfo is one entry in `a2a contracts`' listing (OP-221 second clause): provider, version, state — resolved via publish events (D-023).

type ContractVersion added in v0.13.0

type ContractVersion struct {
	Version       string `json:"version"`
	State         string `json:"state"`
	Sunset        string `json:"sunset,omitempty"`
	Successor     string `json:"successor,omitempty"`
	DeprecationID string `json:"deprecation_id,omitempty"`
}

ContractVersion is one version of a contract and the state it holds, for ContractInfo.Versions. A struct rather than a map so the order is part of the value — semver ascending, which a map cannot express and which every renderer would otherwise have to re-derive.

type EventSummary

type EventSummary struct {
	ULID         string    `json:"ulid"`
	Subject      string    `json:"subject"`
	Transition   string    `json:"transition"`
	ClaimedState string    `json:"claimed_state,omitempty"`
	Actor        string    `json:"actor"`
	ActorSystem  string    `json:"actor_system"`
	At           time.Time `json:"at"`
}

EventSummary is one folded event, for `a2a show`'s event-history rendering.

type Item

type Item struct {
	Space    string   `json:"space"`
	ID       string   `json:"id"`
	Type     string   `json:"type"`
	Title    string   `json:"title"`
	From     string   `json:"from"`
	To       []string `json:"to,omitempty"`
	State    string   `json:"state"`
	Priority string   `json:"priority,omitempty"`
	Blocking bool     `json:"blocking,omitempty"`
	NeededBy string   `json:"needed_by,omitempty"`
	Thread   string   `json:"thread,omitempty"`
	New      bool     `json:"new"`
	Reasons  []string `json:"reasons,omitempty"`

	// PendingMerge is true when a submitted-but-not-yet-visible-as-merged
	// marker exists for this artifact (the pending-merge overlay, §7.2
	// OP-205's "local cache marks pending-merge" step).
	PendingMerge bool `json:"pending_merge,omitempty"`
	// SyncStale is true when the item's own space mirror's sync-age
	// exceeds the statusline TTL — surfaced so `a2a inbox`/`outbox`
	// callers know the data may be behind (T1: "works offline ... with
	// sync age flagged when stale").
	SyncStale bool `json:"sync_stale,omitempty"`
	// LatestEventAt is the timestamp of the artifact's most recent folded
	// event — the "pending since" anchor. Zero when the artifact has no events
	// yet (a bare draft). `json:"-"` deliberately: the dashboard assembler reads
	// this as a Go field (formats it to an age like "5d"), while inbox/outbox
	// `--json` stay byte-stable for their existing consumers (a time.Time would
	// not honor omitempty anyway).
	LatestEventAt time.Time `json:"-"`
	// LatestEventID is the current transition identity for local
	// notification projections. It stays outside the established inbox JSON
	// contract, just like LatestEventAt.
	LatestEventID string `json:"-"`
	// Description is a short human-readable summary from the artifact's body —
	// the "what is this" line the dashboard shows under an inbox/outbox item.
	// json:"-" (like LatestEventAt): a dashboard-only Go field, so inbox/outbox
	// `--json` stay byte-stable for their existing consumers.
	Description string `json:"-"`
	// YourMove is the canonical "whose move is it" projection for this
	// artifact. It is dashboard-only for now: the stable inbox/outbox JSON
	// shape remains unchanged while presentation code stops inferring action
	// ownership from blocking/deadline labels.
	YourMove bool `json:"-"`
}

Item is the JSON shape `a2a inbox`/`a2a outbox`/`a2a search`/`a2a contracts` all guarantee (OP-207/OP-208 "JSON output guaranteed"; snake_case tags, the P6/validate convention). Reasons names which normative condition(s) matched — debuggability, not part of the guaranteed-stable core fields.

type NextAction added in v0.10.0

type NextAction struct {
	Transition string   `json:"transition"`
	By         []string `json:"by"`
}

NextAction is one legal transition and the SYSTEM IDS (never role names) that may make it — fold.LegalNext's Role resolved against the correct envelope (§T3's own "highest-stakes step": for a response member, the PARENT's envelope, never the response's own).

type NotificationSnapshot added in v0.15.0

type NotificationSnapshot struct {
	Items  []Item
	Level  StatuslineResult
	Update UpdateNotice
}

NotificationSnapshot is the cursor-neutral canonical input for local native/editor presentation. Update is included as structured data so an adapter never grades business severity by parsing prose.

type OpenItem added in v0.10.0

type OpenItem struct {
	ID          string       `json:"id"`
	Type        string       `json:"type"`
	State       string       `json:"state"`
	Blocking    bool         `json:"blocking"`
	NeededBy    string       `json:"needed_by,omitempty"`
	NextActions []NextAction `json:"next_actions"`
	WaitingOn   []string     `json:"waiting_on"`
	YourMove    bool         `json:"your_move"`
}

OpenItem answers "whose move is it" from facts for one non-terminal thread member (§T3).

type PendingMarker

type PendingMarker struct {
	ArtifactID string    `json:"artifact_id"`
	Branch     string    `json:"branch"`
	PRNumber   int       `json:"pr_number"`
	PRURL      string    `json:"pr_url"`
	CommitSHA  string    `json:"commit_sha"`
	State      string    `json:"state"`
	MarkedAt   time.Time `json:"marked_at"`
}

PendingMarker is the on-disk shape internal/cli/cache_wiring.go's real PendingMarker adapter writes on every successful submit (§7.2 OP-205's "local cache marks pending-merge" step) — the pending-merge overlay this package reads back and folds into Item.PendingMerge. Format is this package's own (machine-local, gitignored, rebuildable, D-001): losing it only means a submitted-but-unmerged item stops showing the overlay flag until the PR merges and its event lands on `main`.

func ReadMarker added in v0.15.0

func ReadMarker(cacheDir, spaceID, artifactID string) (PendingMarker, error)

ReadMarker reads one artifact's pending marker.

func ReadMarkers

func ReadMarkers(cacheDir, spaceID string) ([]PendingMarker, error)

ReadMarkers lists every pending marker recorded for spaceID under cacheDir. A never-created "pending" directory is not an error (nothing pending yet).

type ProtocolFlagInfo added in v0.13.0

type ProtocolFlagInfo struct {
	Space     string
	System    string
	Artifact  string
	Code      string
	EventULID string
	Message   string
	Severity  string
}

ProtocolFlagInfo is one committed fold violation surfaced by the read model. It deliberately carries fold's own stable vocabulary rather than pretending to be a V4/V5 validation result: those invocations are a different engine and require their own mounted context.

type RefFact

type RefFact struct {
	Ref            string `json:"ref"`
	ID             string `json:"id"`
	Version        string `json:"version,omitempty"`
	PinnedDigest   string `json:"pinned_digest,omitempty"`
	ResolvedDigest string `json:"resolved_digest,omitempty"`
	Resolved       bool   `json:"resolved"`
	DigestMismatch bool   `json:"digest_mismatch"`
}

RefFact is one envelope `refs[]` entry's resolved digest/staleness FACT (never a registry code — internal/cache stays validate-free per ADR-001; internal/cli/cmd_show.go maps this to the V5 registry code).

type SearchFilters

type SearchFilters struct {
	Type  string
	Space string
	State string
	// Thread narrows to one conversation. It lives HERE rather than as a
	// client-side pass over the returned items so both surfaces filter with
	// one piece of code — the CLI and MCP read paths are otherwise
	// independent by ADR-001, and a filter written twice is a filter that
	// can disagree with itself.
	Thread string
}

SearchFilters narrows `a2a search`'s free-text match.

type Severity

type Severity int

Severity is `a2a statusline`'s exit-code contract (§7.5, quoted verbatim): "Exit code communicates severity (0 quiet / 10 items pending / 11 p1 or gate pending) so harnesses can style without parsing."

const (
	SeverityQuiet        Severity = 0
	SeverityItemsPending Severity = 10
	SeverityUrgent       Severity = 11
)

Severity constants map to §7.5's exit codes.

func SeverityOf added in v0.13.0

func SeverityOf(items []Item) Severity

SeverityOf grades an already-computed item list on §7.5's scale, so a caller that is not the statusline can answer "is there anything for me, and how bad is it?" without parsing output.

It exists because an unattended runner had no way to ask. `a2a inbox`'s exit codes are 2=usage, 1=a mirror could not be read, 0=everything else — so "nothing to do" and "five p1 items" are the same code, and any scheduled agent loop has to shell out through `--json | jq length` to find out whether to start work at all. The severity vocabulary was already specified (§7.5, OP-215) and already implemented for the statusline; only its reach was missing.

The grading rule is `urgencyLabel`'s, unchanged and un-forked — same function, so the statusline segment and a scheduler's branch can never disagree about what "urgent" means.

One ceiling, stated because it is a reasonable thing to expect and it is deliberately absent: an item whose `needed_by` has passed grades as ItemsPending, not Urgent. §7.5's exit-code contract is quoted in the plan as "0 quiet / 10 items pending / 11 p1 or gate pending", and widening urgency to cover a missed deadline would change what `a2a statusline` returns on a corpus nobody re-verified. That is a protocol change and belongs in the surfaced-diff flow, not here.

type ShowResult

type ShowResult struct {
	Space string `json:"space"`
	// Path is the space-relative canonical artifact path. It is dashboard-only:
	// the stable CLI JSON contract does not need a repository layout detail,
	// while a human-facing source link must point at the exact committed file.
	Path  string   `json:"-"`
	ID    string   `json:"id"`
	Type  string   `json:"type"`
	Title string   `json:"title"`
	From  string   `json:"from"`
	To    []string `json:"to,omitempty"`
	State string   `json:"state"`
	Body  string   `json:"body"`
	// Thread is the §3.8 conversation this artifact belongs to. Surfaced by
	// `a2a show` so an agent holding one id can reach the whole exchange —
	// without it, the only way to find a conversation is to already know its
	// thread id, which is the discovery dead-end spec 46 records as D6.
	Thread    string         `json:"thread,omitempty"`
	Digest    string         `json:"digest"`
	Events    []EventSummary `json:"events"`
	Flags     []string       `json:"flags,omitempty"`
	Refs      []RefFact      `json:"refs,omitempty"`
	SyncStale bool           `json:"sync_stale"`
	SyncAge   string         `json:"sync_age,omitempty"`
	// Envelope is the heterogeneous, schema-owned frontmatter projection the
	// HTML detail panel needs. It remains outside `a2a show --json` so that
	// command's established output contract stays byte-compatible. Values are
	// untrusted presentation data; renderers must treat them as text only.
	Envelope map[string]any `json:"-"`
}

ShowResult is `a2a show <ref>`'s full output shape (OP-209): artifact body + folded state + event list + facts a V5 code lookup needs.

type SkippedFile added in v0.11.0

type SkippedFile struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
}

SkippedFile is one mirror file (artifact or event) that walkArtifacts or walkEvents could not fold into a space's read index — the read-model's walk stays best-effort (one bad file must never blind the whole space to every other artifact in it), but the file it drops is reported here rather than vanishing without a trace. Path is space-relative (git-style forward slashes, like rawArtifact.RelPath/rawEvent.RelPath); Reason is one of the fixed SkipReason* vocabulary below — never a wrapped Go error string, because the reason is read by agents and a message whose exact text depends on the yaml library's version is not a stable contract.

func BuildArtifactIndex added in v0.13.0

func BuildArtifactIndex(dir string) (map[string]ArtifactIndexEntry, []SkippedFile, error)

BuildArtifactIndex walks dir (a mirror clone's working tree, or any directory laid out the same way) via walkArtifacts and returns id -> ArtifactIndexEntry plus the same []SkippedFile report SkippedFiles/ AllSkippedFiles already expose for the read model — one bad file is dropped, never silently, and never blinds the walk to every other artifact in the tree (walkArtifacts' own doc comment).

Digest is captured AT WALK TIME (from the same bounded read decodeArtifactFile already performed to decode the envelope), not re-read per lookup — a deliberate simplification over the two former adapter-local walks, which re-read the file from disk on every Digest() call after building their own id/path/thread index once. A resolver's observed lifetime is one CLI/MCP invocation with no concurrent mutation of the mirror clone expected mid-process, so the two never actually differed in practice; this trades a second full-file read per Digest() call for a single walk-time read, and is reported as this phase's own deviation rather than assumed harmless.

An id colliding across two files (should never happen in a well-formed mirror — §3.3's id scheme is meant to be unique) keeps whichever walkArtifacts visited last, the same last-write-wins behavior the two former adapter-local walks already had (neither guarded against it either).

type SpaceMirror

type SpaceMirror struct {
	SpaceID  string
	Dir      string
	RepoURL  string
	Manifest space.Manifest
}

SpaceMirror is one connected space's local read surface: its mirror clone's working directory (already cloned/fetched via space.CloneOrFetch) and its structurally-parsed manifest. cmd/a2a (lead, post-wave) builds one of these per space.ProjectConfig.Spaces entry.

type SpaceSyncInfo added in v0.13.0

type SpaceSyncInfo struct {
	Space    string
	Revision string
	Age      time.Duration
	Synced   bool
	Stale    bool
}

SpaceSyncInfo is the mirror snapshot fact the dashboard needs per connected space: exact HEAD revision plus freshness. The computation stays in cache because git inspection, mirrorSyncAge and Store.ttl are cache-owned policy; renderers must not re-read .git or duplicate the stale threshold.

type StatuslineResult

type StatuslineResult struct {
	Line        string `json:"-"`
	New         int    `json:"new"`
	Urgent      int    `json:"urgent"`
	UrgentKind  string `json:"urgent_kind"`
	UrgentID    string `json:"urgent_id"`
	UrgentTitle string `json:"urgent_title"`
	Stale       int    `json:"stale"`
	Update      string `json:"update"`
	Severity    int    `json:"severity"`
	Quiet       bool   `json:"quiet"`
	Exit        int    `json:"-"`
	// contains filtered or unexported fields
}

StatuslineResult is the single computed statusline model. Line preserves the published default text contract; the structured fields power --json and alternate rendering without parsing that text back into facts.

func SampleStatusline added in v0.15.0

func SampleStatusline() StatuslineResult

SampleStatusline returns a deterministic, dependency-free result for shell prompt integration checks. It uses the same model and renderer as live data.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is this package's facade: every P7 read verb (inbox/outbox/ show/thread/search/contracts/statusline) is a thin call into one of its methods. It composes internal/fold over every connected space's mirror on each call — no long-lived in-memory index survives between calls (D-001: nothing this package cannot rebuild from git + the on-disk cursor/marker files is kept anywhere).

func NewStore

func NewStore(ownSystem, cacheDir string, spaces []SpaceMirror, now Clock, ttl time.Duration) *Store

NewStore constructs a Store. ownSystem is this project's configured own system id (§7.4); cacheDir is `.a2a/cache/`'s path (cursor + pending-marker files live under it — never spaces' mirror working trees, which cacheDir's own "mirrors/" sibling subdir already owns per space.ResolveMirrorLocation's default). now must not be nil (rails anti-pattern #10). ttl is the statusline refresh TTL (§7.5); zero defaults to DefaultStatuslineTTL.

func (*Store) AllSkippedFiles added in v0.11.0

func (s *Store) AllSkippedFiles(ctx context.Context) (map[string][]SkippedFile, error)

AllSkippedFiles is SkippedFiles for every connected space at once (space id -> its sorted skipped files) — `a2a inbox` is not space-scoped the way `a2a thread` is, so it needs the union across every connected mirror rather than one space's slice. See SkippedFiles' doc comment for why this pair of accessors exists at all.

func (*Store) AvatarDataURL added in v0.18.2

func (s *Store) AvatarDataURL(login string) (string, bool)

AvatarDataURL reads one validated, project-local avatar cache record. It is intentionally a Store capability so HTML does not learn the cache layout; the false result is a normal monogram fallback, never a render failure.

func (*Store) ClaimStatuslineRefreshLease added in v0.16.3

func (s *Store) ClaimStatuslineRefreshLease() bool

ClaimStatuslineRefreshLease atomically rate-limits detached refreshes across separate one-shot statusline processes. A successful sync removes the lease; the TTL recovers from a child that was interrupted before cleanup.

func (*Store) Contracts

func (s *Store) Contracts(ctx context.Context, provider string) ([]ContractInfo, error)

Contracts computes `a2a contracts [--provider <sys>]` (OP-221 second clause): every KindContract artifact known to the local cache, its provider (envelope `from`), latest recorded `publish` event's version (D-023), and its folded state.

func (*Store) EnableUpdateNotice

func (s *Store) EnableUpdateNotice(binaryVersion, cachePath string, ttl time.Duration, checker func(context.Context))

EnableUpdateNotice turns on the T3/T4 update-notice mechanism on an already-constructed Store: a post-construction setter, deliberately NOT a NewStore parameter, so every existing call site's behavior (and Statusline's byte output) is unaffected until a caller opts in. binaryVersion is this build's bare current version (release.Resolve's "current"); cachePath is the T3 machine-level update-check cache file (release.CachePath()); ttl is the cache's freshness window (<=0 defaults to DefaultUpdateCheckTTL); checker is the background refresh function (typically release.NewChecker's return value) triggerUpdateRefreshIfStale spawns detached when the cache is stale — nil disables the refresh trigger while still allowing UpdateNotice to render from whatever the cache already holds. StatuslineRefreshNeeded reads checker presence and cache freshness, while the CLI-owned detached `a2a sync` invokes the canonical checker out of process.

func (*Store) Inbox

func (s *Store) Inbox(ctx context.Context, actionableOnly bool) ([]Item, error)

Inbox computes `a2a inbox` (OP-207): actionableOnly=false is the base §4.2 "to includes me" query; actionableOnly=true applies the normative 5-condition `--actionable` union instead (a different, broader query — see actionableReasons' doc comment). Every call advances the per-system read cursor (spec 07 "what to do" #2: "inbox ... advances the read cursor on run" — unqualified by the flag).

func (*Store) InboxSnapshot added in v0.15.0

func (s *Store) InboxSnapshot(ctx context.Context, actionableOnly bool) ([]Item, error)

InboxSnapshot computes the same read model as Inbox without advancing the human read cursor. Passive presentation surfaces such as local HTML use this method; OP-207's explicit inbox read remains the only cursor writer.

func (*Store) NotificationItems added in v0.15.0

func (s *Store) NotificationItems(ctx context.Context) ([]Item, error)

NotificationItems returns the current semantic signal set without advancing the OP-207 human read cursor. It is deliberately a level projection: per-channel transition/newness belongs to internal/notification's independent delivery state.

func (*Store) NotificationSnapshot added in v0.15.0

func (s *Store) NotificationSnapshot(ctx context.Context) (NotificationSnapshot, error)

NotificationSnapshot returns current state without waiting on mirror refresh and without writing the human cursor. Unlike the prompt-budgeted statusline, notification consumers are short-lived processes: they synchronously run one bounded stale update refresh so a native-only installation can discover a release before the process exits.

func (*Store) Outbox

func (s *Store) Outbox(ctx context.Context, attentionOnly bool) ([]Item, error)

Outbox computes `a2a outbox` (OP-208): attentionOnly=false lists every own OPEN item; attentionOnly=true applies the normative 4-condition `--attention` union. Outbox never advances the read cursor itself (only `a2a inbox` does, per OP-207's own wording) — it reads whatever cursor snapshot the last inbox run left behind.

func (*Store) OutboxSnapshot added in v0.17.1

func (s *Store) OutboxSnapshot(ctx context.Context) ([]Item, error)

OutboxSnapshot returns every own open item without consuming cursor state, annotated with all current attention reasons and canonical move ownership for passive presentation surfaces such as the local dashboard.

func (*Store) Overdue added in v0.13.0

func (s *Store) Overdue(ctx context.Context) ([]Item, error)

Overdue returns the open artifacts addressed to this system whose `needed_by` date has passed — the work that is late and MINE, regardless of whether I have acknowledged it.

It deliberately does NOT advance the read cursor, unlike Inbox. The whole point of this query is to be safe to run on a timer, and Inbox's cursor advance is what turns "new" off for the next reader: a scheduled poll every ten minutes would quietly consume the `[new]` marks and the statusline's "N new" count before any human saw them. A diagnostic read must not spend the signal it is diagnosing.

`needed_by` is an optional date-only field (`YYYY-MM-DD`). An artifact without one, or with one this cannot parse, is never overdue — the question is whether a stated deadline has passed, and a malformed date states nothing. It is not reported as an error here either: the schema owns field validity, and a read verb that refused to answer because a counterparty wrote a bad date would be the silence this file exists to remove.

func (*Store) OwnSystem added in v0.2.0

func (s *Store) OwnSystem() string

OwnSystem returns this project's configured own system id (the dashboard's ego node / --system default).

func (*Store) ProtocolFlags added in v0.13.0

func (s *Store) ProtocolFlags(ctx context.Context) ([]ProtocolFlagInfo, error)

ProtocolFlags returns every unique non-fatal fold violation found while rebuilding the connected spaces. These are committed-history facts, not V1/V2/V3 validation results: the offending event stays in history and fold reports why it did not apply cleanly.

func (*Store) ReleaseStatuslineRefreshLease added in v0.16.3

func (s *Store) ReleaseStatuslineRefreshLease()

ReleaseStatuslineRefreshLease releases this Store's claimed lease after a detached process failed to start.

func (*Store) Search

func (s *Store) Search(ctx context.Context, query string, filters SearchFilters) ([]Item, error)

Search computes `a2a search <query> [--type --space --state]` (OP-221 first clause): a case-insensitive substring match over id/title/body, hub-less, local-cache only. Zero hits returns an empty (non-nil) slice, never an error (spec §6: "empty result, not error").

func (*Store) SetCloneOrFetchForTest added in v0.8.0

func (s *Store) SetCloneOrFetchForTest(f func(ctx context.Context, dir, repoURL string) error)

SetCloneOrFetchForTest overrides the injected mirror-refresh seam (test-only DI, rails anti-pattern #10 convention — same shape as feedback.Submitter.SetCloneOrFetchForTest). Production always uses NewStore's own space.CloneOrFetch default; refresh_test.go uses this to inject a call-counting / failing fake without a real network fetch.

func (*Store) Show

func (s *Store) Show(ctx context.Context, ref string) (ShowResult, error)

Show computes `a2a show <ref>` (OP-209): ref is a §5.7 ref grammar string (`id`, `id@version`, `id#digest`, or a `space:id...` cross- space form — only the bare `id` segment resolves the target here; version/digest are compared against, not used to select, since D-023 resolves versions through publish events, and this package's own per-artifact history is already available via Result/Events).

func (*Store) ShowMany added in v0.17.1

func (s *Store) ShowMany(ctx context.Context, refs []string) ([]ShowResult, error)

ShowMany resolves several artifact detail records from one composed cache index. Dashboard assembly uses this instead of calling Show in a loop, which would re-walk and re-fold every connected mirror once per visible row. Results preserve refs order; callers should pass space-qualified refs when the source record already carries a space.

func (*Store) SkippedFiles added in v0.11.0

func (s *Store) SkippedFiles(ctx context.Context, spaceID string) ([]SkippedFile, error)

SkippedFiles reports, for one connected space, every mirror file this package's best-effort walk could not fold into that space's read index — sorted by path (deterministic, because this feeds golden CLI/MCP output). An unknown spaceID or a space with nothing skipped both return an empty slice, never an error.

This accessor — and AllSkippedFiles below — exist because of a filed defect: walkArtifacts/walkEvents were already best-effort BY DESIGN (one malformed file must never blind the whole space to every other artifact in it), but their silence made a malformed artifact indistinguishable from one that simply does not exist. A `thread:` key written twice in one envelope made yaml.Unmarshal reject it; `search`, `inbox`, `statusline` and `thread` all returned nothing for it, with no error anywhere, while `a2a show` (which parses frontmatter directly, outside this package) still printed it fine. In a shared cross-company record, a counterparty's document dropping out of your index without a word is worse than a hard error — the report, not the skip, was the missing half. This package keeps no long-lived index (D-001), so this recomputes fresh from the mirror on every call, same as every other Store method.

func (*Store) SpaceMirrors added in v0.2.0

func (s *Store) SpaceMirrors() []SpaceMirror

SpaceMirrors returns a read-only view of the connected space mirrors (id, dir, repo URL, manifest) — the dashboard reads nodes (participants), the connected-repo list, and each mirror's dir (to walk consumes.yaml) from here rather than replicating wire.go's buildStore wiring. The slice is the Store's own (callers must not mutate it).

func (*Store) SpaceSyncFacts added in v0.13.0

func (s *Store) SpaceSyncFacts(ctx context.Context) []SpaceSyncInfo

SpaceSyncFacts returns one cache-owned mirror snapshot fact per connected space, in the same order as SpaceMirrors. It keeps exact HEAD revision, mirrorSyncAge and the TTL in their owner instead of making the HTML layer inspect .git metadata or duplicate stale policy.

func (*Store) Statusline

func (s *Store) Statusline(ctx context.Context) (StatuslineResult, error)

Statusline computes §7.5's contract: cache-read only (no network, no live git fetch — every read here is a local working-tree/`.git` read), at most one line, zero-noise (empty line + exit 0 when nothing actionable, or when no space is connected at all — CC-092), EXCEPT that spec 19 T4 amends CC-092: an available update IS actionable content, so an otherwise-empty line prints the update segment alone (still exit 0 — the notice never inflates severity). When any connected space's mirror sync-age exceeds the Store's TTL, the CLI boundary may claim ClaimStatuslineRefreshLease and start the canonical `a2a sync` path. This method itself stays strictly cache-only and owns no background work.

func (*Store) StatuslineRefreshNeeded added in v0.16.3

func (s *Store) StatuslineRefreshNeeded() bool

StatuslineRefreshNeeded reports whether the next statusline render should request a refresh. It only reads local cache metadata; it never starts work.

func (*Store) SyncIfStale added in v0.8.0

func (s *Store) SyncIfStale(ctx context.Context) []error

SyncIfStale refreshes every connected space's mirror whose sync-age exceeds the Store's TTL (AC-1050, spec 45 M1): the read path's own fix for a contract published on the other side after this side's last `sync` being silently invisible to `a2a inbox`.

Every clause here is load-bearing:

  1. Refresh only, NEVER first-clone. A mirror whose Dir is not already a git repository is skipped — not cloned. space.CloneOrFetch clones into an absent/empty dir, and a clone this func's own timeout kills mid-write can leave a non-empty NON-git directory, which every later call then refuses permanently with space.ErrNonGitTarget (internal/space/mirror.go). A read verb must never be able to poison a mirror that way. A missing mirror stays doctor/sync/ connect's job; buildStore already copes with a zero manifest.

  2. Staleness reuses mirrorSyncAge, but against readRefreshTTL — NOT the Store's statusline TTL. Never-synced (mirrorSyncAge's synced=false) counts as stale, same as spaceSyncStale's own convention.

    Sharing the statusline's TTL was a real defect, found end-to-end on 2026-07-26 against a live space: an artifact whose pull request had just merged was INVISIBLE to `a2a outbox` and `a2a show`, which answered "artifact not found", and one explicit `a2a sync` revealed it immediately. The mirror had been fetched moments earlier by the submit, so its age was inside DefaultStatuslineTTL (5 minutes) and this function skipped it as fresh.

    Five minutes is exactly the window in which a counterparty's merge matters most, and the documented session-start loop says an empty inbox means proceed — so this reproduced the very blocker the read refresh was written to close, just narrower. The two callers were never the same question: the statusline renders on every shell prompt and needs a cheap window, while a read verb is a deliberate question asked seconds apart at worst.

  3. Bounded: each attempted mirror gets its own context.WithTimeout(ctx, perMirrorTimeout) derived from ctx; the remaining totalBudget is checked BEFORE starting each mirror, and once it is spent, SyncIfStale stops and returns one error per mirror it never attempted, naming it.

  4. Errors are returned, never logged (rails: log-or-return, never both) — one error per failed mirror, each naming the space id and wrapped so errors.Is still resolves the underlying cause.

  5. Never fatal to the read: a failure here never panics and never leaves the mirror less usable than it was — see this method's own doc trailer on what a timed-out fetch does to the mirror on disk.

  6. A successful fetch also refreshes this Store's own cached space.Manifest for that mirror (best-effort; a fetch that succeeds but leaves an unreadable/unparseable space.yaml keeps the prior manifest rather than erroring the fetch over it). Without this, buildIndex (mirror.go) would find a just-fetched participant's freshly-published artifact just fine — walkArtifacts/walkEvents are generic tree walks, never manifest-scoped — but fold.Fold would resolve that participant's own entry-transition authorization (internal/fold's membership check) against the manifest AS IT STOOD BEFORE the fetch. A participant who joins the space and publishes their first artifact in the same push would fold to an unauthorized-actor flag stuck at `draft`, not the visible-but-wrong "invisible artifact" this whole method exists to kill, just one layer downstream of it.

A timed-out git fetch (this func's own ctxDerived expiring mid-run) kills the git process (exec.CommandContext's own SIGKILL-on-cancel); space.CloneOrFetch's fetch step only advances refs after git's fetch completes successfully, and its checkoutRemoteHead step (the one thing that touches the WORKING TREE) never runs at all when fetch itself returns an error. So a killed fetch leaves the mirror's refs and working tree exactly as they were before the call — this method reads nothing else from Store's own state, so there is nothing else to leave inconsistent.

func (*Store) Thread

func (s *Store) Thread(ctx context.Context, threadID string) ([]Item, error)

Thread computes `a2a thread <thread-id>` (OP-210): every artifact across every connected space whose envelope `thread` field matches, ordered by creation (earliest first) via the same commit-order fold already resolved (LatestEventAt is used as the ordering proxy — the entry event's own timestamp).

func (*Store) ThreadView added in v0.10.0

func (s *Store) ThreadView(ctx context.Context, ref string, spaceID string) (ThreadResult, error)

ThreadView computes `a2a thread <thread-id | any-member-artifact-id> [--space <id>]` (OP-210, spec 46 §T3/§T4). ref is either a `thread:...` value (used directly) or an artifact id (resolved to that artifact's thread, recorded in ResolvedFrom) — an id matching nothing in the searched space(s) is ErrNotFound, never an empty success (D6). spaceID, when non-empty, restricts BOTH the any-member resolution and the rendered space to that one connected space; when empty, resolution searches every connected space and membership is computed per-space (CC-073) — present in exactly one renders, present in two or more is a *ThreadAmbiguityError, present in zero is ErrThreadNotFound.

func (*Store) UpdateDetail added in v0.15.0

func (s *Store) UpdateDetail() release.DetailCache

UpdateDetail returns only a previously verified, target-bound release notes projection. It never refreshes the network and never exposes prose from a malformed, unverified, tampered, or future-schema cache record.

func (*Store) UpdateNotice

func (s *Store) UpdateNotice() UpdateNotice

UpdateNotice computes the T4 shared advisory fact from the T3 cache: zero value (Grade GradeNone) when EnableUpdateNotice was never called, or when the cache holds no known "latest" fact at all. Freshness (TTL) only gates the background REFRESH (triggerUpdateRefreshIfStale) — a stale-but-present cache value is still rendered here, per release.ReadLatest's own doc comment.

type ThreadAmbiguityError added in v0.10.0

type ThreadAmbiguityError struct {
	Thread string
	Spaces []ThreadSpaceHit
}

ThreadAmbiguityError is returned by ThreadView when threadID has members in TWO OR MORE connected spaces — CC-073 forbids the silent merge; T4 requires a refusal carrying enough for the caller (the CLI wave) to print a recovery, never a partitioned render (T4: "sectioned output *will* be read as one conversation by a weak agent").

func (*ThreadAmbiguityError) Error added in v0.10.0

func (e *ThreadAmbiguityError) Error() string

type ThreadArtifact added in v0.10.0

type ThreadArtifact struct {
	ID     string      `json:"id"`
	Type   string      `json:"type"`
	From   string      `json:"from"`
	To     []string    `json:"to,omitempty"`
	Title  string      `json:"title"`
	State  string      `json:"state"`
	Parent string      `json:"parent,omitempty"`
	Refs   []ThreadRef `json:"refs,omitempty"`
}

ThreadArtifact is one member's projection in ThreadResult.Artifacts — current folded state lives HERE and in OpenItems, never duplicated onto a TranscriptEntry (§T3 "one state per fact").

type ThreadFlag added in v0.10.0

type ThreadFlag struct {
	Kind      string `json:"kind"`
	Subject   string `json:"subject"`
	EventULID string `json:"event_ulid,omitempty"`
}

ThreadFlag is one fold.Flag attributable to a thread member, rendered (never dropped — §T3 "flags ... ALWAYS present").

type ThreadOpener added in v0.10.0

type ThreadOpener struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	From  string `json:"from"`
}

ThreadOpener is the COMPUTED lowest-seq (or, in declared-order mode, earliest-created) member of a thread — never a declared root (§T3).

type ThreadRef added in v0.10.0

type ThreadRef struct {
	Ref string `json:"ref"`
}

ThreadRef is one artifact's `refs[]` entry as rendered in ThreadResult. Ref digest verdicts are deliberately NOT resolved here (§T3 "Not included" — `a2a show` owns that); this is the bare grammar string.

type ThreadResult added in v0.10.0

type ThreadResult struct {
	Thread       string            `json:"thread"`
	Space        string            `json:"space"`
	Order        string            `json:"order"`
	ResolvedFrom string            `json:"resolved_from,omitempty"`
	Opener       ThreadOpener      `json:"opener"`
	Participants []string          `json:"participants"`
	SyncStale    bool              `json:"sync_stale"`
	Artifacts    []ThreadArtifact  `json:"artifacts"`
	Transcript   []TranscriptEntry `json:"transcript"`
	OpenItems    []OpenItem        `json:"open_items"`
	Flags        []ThreadFlag      `json:"flags"`
	Unresolved   []UnresolvedFact  `json:"unresolved"`
}

ThreadResult is `a2a thread`'s full read model (spec 46 §T3/§T4): one causally-ordered transcript of artifacts AND events for ONE thread in ONE space, plus an "open_items" whose-move-is-it block — composed over Store's own index + internal/fold, never a second traversal or a second reading of the transition table. This is a NEW result type, deliberately independent of Item (inbox/outbox/search's guaranteed-stable shape is untouched).

type ThreadSpaceHit added in v0.10.0

type ThreadSpaceHit struct {
	Space       string
	MemberCount int
	Opener      ThreadOpener
}

ThreadSpaceHit is one connected space's summary in a ThreadAmbiguityError (T4's disambiguation block: "each space, its member count, its opener").

type TranscriptArtifact added in v0.10.0

type TranscriptArtifact struct {
	ID    string   `json:"id"`
	Type  string   `json:"type"`
	From  string   `json:"from"`
	To    []string `json:"to,omitempty"`
	Title string   `json:"title"`
}

TranscriptArtifact is a transcript entry's artifact-kind payload — a deliberately minimal projection (no state: see ThreadArtifact's doc).

type TranscriptEntry added in v0.10.0

type TranscriptEntry struct {
	Seq      int64               `json:"seq"`
	Kind     string              `json:"kind"`
	At       time.Time           `json:"at"`
	Artifact *TranscriptArtifact `json:"artifact,omitempty"`
	Event    *TranscriptEvent    `json:"event,omitempty"`
}

TranscriptEntry is ONE strictly seq-ordered transcript row — a discriminated union (Kind = "artifact" | "event"), never two lists a reader must interleave mentally (§T3).

type TranscriptEvent added in v0.10.0

type TranscriptEvent struct {
	ULID         string               `json:"ulid"`
	Subject      string               `json:"subject"`
	Transition   string               `json:"transition"`
	ClaimedState string               `json:"claimed_state,omitempty"`
	Actor        TranscriptEventActor `json:"actor"`
	// ResponseID is set only on a `respond` event (D-024's newly attached
	// response id).
	ResponseID string `json:"response_id,omitempty"`
}

TranscriptEvent is a transcript entry's event-kind payload.

type TranscriptEventActor added in v0.10.0

type TranscriptEventActor struct {
	Kind   string `json:"kind"`
	Name   string `json:"name"`
	System string `json:"system"`
}

TranscriptEventActor is one event's actor block, as rendered in a transcript entry.

type UnresolvedFact added in v0.10.0

type UnresolvedFact struct {
	Kind string `json:"kind"` // "parent" | "event-subject"
	ID   string `json:"id"`
}

UnresolvedFact is one reference this thread's own rendered member set could not resolve — a response whose `parent` is not itself a rendered member, or an event whose `subject` is not (§T3 "rendered, never dropped"; CC-073/REF-009 mean this should be empty on a conforming thread — its presence signals a fork or a broken propagation).

type UpdateNotice

type UpdateNotice struct {
	Current         string    `json:"current"`
	Latest          string    `json:"latest"`
	UpdateAvailable bool      `json:"update_available"`
	Floor           string    `json:"floor"`
	FloorSpace      string    `json:"floor_space"`
	Required        bool      `json:"required"`
	CheckedAt       time.Time `json:"checked_at"`
	Source          string    `json:"source"`
	Fresh           bool      `json:"fresh"`
	ReleaseURL      string    `json:"release_url"`

	Grade    release.Grade `json:"-"`
	Segment  string        `json:"-"`
	Sentence string        `json:"-"`
}

UpdateNotice is the T4 shared advisory fact every Store-based surface (statusline here; inbox/outbox/mcp in wave 12c) renders from — one struct, one computation (Store.UpdateNotice), so every surface's wording and grading stay in sync. JSON tags match spec 19 T1's `--json` / `a2a_read` `update` object shape verbatim: {current, latest, update_available, floor, floor_space, required}. Grade/Segment/Sentence are in-process rendering helpers, not part of that wire shape.

Jump to

Keyboard shortcuts

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