thread

package
v0.36.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 10 Imported by: 0

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

View Source
const (
	StatusOpen     = "open"
	StatusResolved = "resolved"
)

Status values for a thread.

Variables

This section is empty.

Functions

func Marshal added in v0.16.0

func Marshal(threads []Thread) ([]byte, error)

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

func ParseMentions(body string) []string

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

func Merge(a, b []Thread) []Thread

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

func Parse(data []byte) ([]Thread, error)

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

func (t Thread) IsOpen() bool

IsOpen reports whether the thread is still awaiting resolution.

func (Thread) LastActivity added in v0.35.0

func (t Thread) LastActivity() time.Time

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

func (t Thread) Participants() []string

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

Jump to

Keyboard shortcuts

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