clone

a lazygit-style terminal viewer for openspec repos: watch changes, tasks and spec deltas land as agents work

main
lazyopenspec / internal / spec / model.go
model.go 3.8K
1// Package spec reads an OpenSpec tree from disk into an immutable Snapshot.2package spec34import (5 "strings"6 "time"7)89// ArtifactKind classifies a file inside a change directory.10type ArtifactKind int1112const (13 KindProposal ArtifactKind = iota14 KindDesign15 KindTasks16 KindDelta17 KindOther18)1920func (k ArtifactKind) String() string {21 switch k {22 case KindProposal:23 return "proposal"24 case KindDesign:25 return "design"26 case KindTasks:27 return "tasks"28 case KindDelta:29 return "delta"30 default:31 return "other"32 }33}3435// Artifact is a single markdown file belonging to a change.36type Artifact struct {37 Kind ArtifactKind38 Rel string // path relative to the change dir, e.g. "specs/auth/spec.md"39 Path string // absolute path40 ModTime time.Time41 Content string42}4344// Label is the short display name of the artifact.45func (a Artifact) Label() string {46 if a.Kind == KindDelta {47 return "Δ " + a.Capability()48 }49 return a.Rel50}5152// Capability is the spec ID a delta artifact targets: specs/<id>/spec.md -> <id>.53func (a Artifact) Capability() string {54 return strings.TrimSuffix(strings.TrimPrefix(a.Rel, "specs/"), "/spec.md")55}5657// TaskStats counts markdown checkboxes in tasks.md.58type TaskStats struct {59 Done int60 Total int61}6263// Complete reports whether there is at least one task and all are checked.64func (t TaskStats) Complete() bool { return t.Total > 0 && t.Done == t.Total }6566// DeltaStats counts requirement operations across a change's delta specs.67type DeltaStats struct {68 Added int69 Modified int70 Removed int71 Renamed int72}7374// Total is the number of requirement operations.75func (d DeltaStats) Total() int { return d.Added + d.Modified + d.Removed + d.Renamed }7677func (d DeltaStats) add(o DeltaStats) DeltaStats {78 return DeltaStats{79 Added: d.Added + o.Added,80 Modified: d.Modified + o.Modified,81 Removed: d.Removed + o.Removed,82 Renamed: d.Renamed + o.Renamed,83 }84}8586// Change is an active or archived change directory.87type Change struct {88 ID string // directory name89 Dir string90 Archived bool91 ArchivedOn string // YYYY-MM-DD prefix for archived changes92 Name string // ID without the archive date prefix93 Schema string // from .openspec.yaml, if present94 Summary string // first prose line under "## Why" in proposal.md95 Artifacts []Artifact96 Tasks TaskStats97 Deltas DeltaStats98 ModTime time.Time // newest artifact mtime99}100101// Artifact returns the first artifact of the given kind.102func (c Change) Artifact(kind ArtifactKind) (Artifact, bool) {103 for _, a := range c.Artifacts {104 if a.Kind == kind {105 return a, true106 }107 }108 return Artifact{}, false109}110111// Has reports whether the change has an artifact of the given kind.112func (c Change) Has(kind ArtifactKind) bool {113 _, ok := c.Artifact(kind)114 return ok115}116117// Spec is a main capability spec under openspec/specs.118type Spec struct {119 ID string // capability directory name120 Path string // absolute path to spec.md121 Title string122 Purpose string123 Requirements int124 Scenarios int125 ModTime time.Time126 Content string127}128129// Snapshot is a point-in-time read of an OpenSpec tree.130type Snapshot struct {131 Root string // repository root (parent of openspec/)132 Dir string // the openspec/ directory133 Exists bool // whether Dir exists134 Changes []Change135 Archive []Change136 Specs []Spec137 Config string // path to config.yaml or project.md, if any138}139140// FindChange looks up an active or archived change by ID.141func (s *Snapshot) FindChange(id string) (Change, bool) {142 for _, c := range s.Changes {143 if c.ID == id {144 return c, true145 }146 }147 for _, c := range s.Archive {148 if c.ID == id {149 return c, true150 }151 }152 return Change{}, false153}154155// FindSpec looks up a main spec by capability ID.156func (s *Snapshot) FindSpec(id string) (Spec, bool) {157 for _, sp := range s.Specs {158 if sp.ID == id {159 return sp, true160 }161 }162 return Spec{}, false163}