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
- func Marshal(threads []Thread) ([]byte, error)
- func ParseMentions(body string) []string
- type Reply
- type SidecarStore
- func (s *SidecarStore) Create(specID, section, author, question string, mentions []string) (Thread, error)
- func (s *SidecarStore) CreateQuoted(specID, section, author, question string, mentions []string, ...) (Thread, error)
- func (s *SidecarStore) List(specID string) ([]Thread, error)
- func (s *SidecarStore) Reanchor(specID, threadID, newSection string) (Thread, error)
- func (s *SidecarStore) Reopen(specID, threadID string) (Thread, error)
- func (s *SidecarStore) Reply(specID, threadID, author, body string, mentions []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 ¶
func Marshal ¶ added in v0.16.0
Marshal serializes a thread set into the deterministic sidecar document shape, applying the same ordering as the on-disk store so merged output diffs cleanly.
func ParseMentions ¶ added in v0.33.0
ParseMentions extracts @handle tokens from a body in first-seen order, deduplicated, with the leading @ stripped. A handle is [A-Za-z0-9_.:-]+ following an @ at a word boundary. Email addresses and mid-word @ are ignored.
Types ¶
type Reply ¶
type Reply struct {
Author string `yaml:"author"`
At time.Time `yaml:"at"`
Body string `yaml:"body"`
Mentions []string `yaml:"mentions,omitempty"`
}
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, mentions []string) (Thread, error)
Create appends a new open thread. mentions are explicit handles from the caller (e.g. a --to flag), unioned with @handles parsed from question.
func (*SidecarStore) CreateQuoted ¶ added in v0.35.0
func (s *SidecarStore) CreateQuoted(specID, section, author, question string, mentions []string, quote, quotePrefix string) (Thread, error)
CreateQuoted appends a new open thread carrying an optional quote anchor: a verbatim text span within the section (plus a disambiguating prefix). An empty quote produces a plain section-level thread, identical to Create.
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) Reanchor ¶ added in v0.35.0
func (s *SidecarStore) Reanchor(specID, threadID, newSection string) (Thread, error)
Reanchor moves a thread to a new section slug. It is the repair path for threads whose section heading was reworded out from under them (the "unanchored" bucket in the reader) — an ordinary sidecar mutation that merges by thread ID like any other.
func (*SidecarStore) Reopen ¶ added in v0.35.0
func (s *SidecarStore) Reopen(specID, threadID string) (Thread, error)
Reopen clears resolution metadata and returns a thread to open status.
func (*SidecarStore) Reply ¶
func (s *SidecarStore) Reply(specID, threadID, author, body string, mentions []string) (Thread, error)
Reply appends a reply to an existing thread. mentions are unioned with @handles parsed from body, same as Create.
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.
// mentions are explicit handles from the caller (e.g. a --to flag); they
// are unioned with @handles parsed from question. Pass nil when there are
// no explicit handles — inline mentions are still parsed either way.
Create(specID, section, author, question string, mentions []string) (Thread, error)
// Reply appends a reply to an existing thread. mentions are unioned with
// @handles parsed from body, same as Create.
Reply(specID, threadID, author, body string, mentions []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)
// Reopen undoes a resolution.
Reopen(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"`
// Mentions are the handles addressed by the question, in first-seen order.
// Derived from the body (and any explicit --to handles) at write time;
// never hand-edited.
Mentions []string `yaml:"mentions,omitempty"`
// Quote is the verbatim text span the thread refers to within Section.
// Optional. When the text no longer appears in the section, the thread
// degrades to a section-level anchor — it is never orphaned.
Quote string `yaml:"quote,omitempty"`
// QuotePrefix is a short run of text immediately before Quote, used to
// disambiguate when Quote appears more than once in the section.
QuotePrefix string `yaml:"quote_prefix,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.
func Parse ¶ added in v0.16.0
Parse decodes a sidecar document body into its thread set. An empty body yields an empty (non-nil-error) result so callers can merge cleanly.
func (Thread) LastActivity ¶ added in v0.35.0
LastActivity returns the thread's most recent activity timestamp: the last reply's time when replies exist, else the creation time. Read-state tracking compares against this so a new reply re-marks a thread unread.
func (Thread) Participants ¶ added in v0.33.0
Participants returns the deduplicated set of handles involved in a thread: the author, every replier, and every mentioned handle. Order is stable (author first, then first-seen).