Documentation
¶
Overview ¶
Package thread provides inline Q&A threads for spec review.
A thread is a lightweight, section-anchored conversation: a question, a list of replies, and an open/resolved flag. Threads are persisted as a sidecar YAML file next to the spec so they ride the existing git-backed specs-repo sync without touching the spec markdown or its frontmatter.
The engine performs no terminal I/O and shells out to nothing. Callers (the CLI, the TUI, and the MCP handler) drive it through the Store interface and render the results themselves.
Index ¶
- Constants
- type Reply
- type SidecarStore
- func (s *SidecarStore) Create(specID, section, author, question string) (Thread, error)
- func (s *SidecarStore) Get(specID, threadID string) (Thread, error)
- func (s *SidecarStore) List(specID string) ([]Thread, error)
- func (s *SidecarStore) Reply(specID, threadID, author, body string) (Thread, error)
- func (s *SidecarStore) Resolve(specID, threadID, by string) (Thread, error)
- func (s *SidecarStore) SidecarPath(specID string) string
- type Store
- type Thread
Constants ¶
const ( StatusOpen = "open" StatusResolved = "resolved" )
Status values for a thread.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Reply ¶
type Reply struct {
Author string `yaml:"author"`
At time.Time `yaml:"at"`
Body string `yaml:"body"`
}
Reply is a single message appended to a thread.
type SidecarStore ¶
type SidecarStore struct {
// contains filtered or unexported fields
}
SidecarStore persists threads as <specsDir>/<SPEC-ID>.threads.yaml.
The file is the only new tracked artifact: it sits beside the spec and syncs through the existing specs-repo git flow. Serialization is deterministic so independent edits diff cleanly and merge associatively.
func NewSidecarStore ¶
func NewSidecarStore(dir string) *SidecarStore
NewSidecarStore returns a store rooted at the given specs directory.
func (*SidecarStore) Create ¶
func (s *SidecarStore) Create(specID, section, author, question string) (Thread, error)
Create appends a new open thread.
func (*SidecarStore) Get ¶
func (s *SidecarStore) Get(specID, threadID string) (Thread, error)
Get returns a single thread by ID.
func (*SidecarStore) List ¶
func (s *SidecarStore) List(specID string) ([]Thread, error)
List loads and returns all threads for a spec. A missing sidecar is not an error — it simply means the spec has no threads yet.
func (*SidecarStore) Reply ¶
func (s *SidecarStore) Reply(specID, threadID, author, body string) (Thread, error)
Reply appends a reply to an existing thread.
func (*SidecarStore) Resolve ¶
func (s *SidecarStore) Resolve(specID, threadID, by string) (Thread, error)
Resolve marks a thread resolved.
func (*SidecarStore) SidecarPath ¶
func (s *SidecarStore) SidecarPath(specID string) string
SidecarPath returns the sidecar file path for a spec ID.
type Store ¶
type Store interface {
// List returns all threads for a spec, in deterministic order.
List(specID string) ([]Thread, error)
// Create appends a new open thread anchored to a section and returns it.
Create(specID, section, author, question string) (Thread, error)
// Reply appends a reply to an existing thread.
Reply(specID, threadID, author, body string) (Thread, error)
// Resolve marks a thread resolved. Resolving an already-resolved thread
// is a no-op that returns the thread unchanged.
Resolve(specID, threadID, by string) (Thread, error)
// Get returns a single thread by ID.
Get(specID, threadID string) (Thread, error)
}
Store is the engine boundary for thread persistence. Callers depend on this interface, never on the concrete backend, so a future backend (e.g. a server or local cache) needs no caller changes.
type Thread ¶
type Thread struct {
// ID is a short, stable, content-independent identifier (e.g. "T-7f3a").
// It never changes, so replies and resolves never collide on renumbering.
ID string `yaml:"id"`
// Section is the markdown section slug the thread is anchored to.
// This is the only anchor in v1 — a thread is never orphaned by line shifts.
Section string `yaml:"section"`
// Status is open or resolved.
Status string `yaml:"status"`
// Author is the handle/name of whoever asked the question.
Author string `yaml:"author"`
// Created is when the question was asked (UTC).
Created time.Time `yaml:"created"`
// Question is the opening message.
Question string `yaml:"question"`
// Replies are appended in chronological order.
Replies []Reply `yaml:"replies,omitempty"`
// ResolvedBy and ResolvedAt are set when the thread is resolved.
ResolvedBy string `yaml:"resolved_by,omitempty"`
ResolvedAt *time.Time `yaml:"resolved_at,omitempty"`
}
Thread is a single section-anchored conversation.
func Merge ¶
Merge reconciles two thread sets into one. It is used to resolve the rare case where two reviewers edited the same sidecar offline.
Strategy:
- Threads are unioned by ID. A thread present in only one side is kept.
- For a thread present in both sides, replies are unioned (same author + timestamp + body counts as the same reply); the resolved state wins if either side resolved it. This makes merges associative and never drops a reply.
The result is returned in deterministic order so a merged file diffs cleanly.