thread

package
v0.34.2 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 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) 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, 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)
}

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"`
}

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