snapshot of 8b9e9fcfc8b13c2d29024f6f756b28bc6851512b Annotations about the code that implements koment.

internal/store/record.go

1 // Package store reads and writes the annotation records that live in .koment.
2 package store
3
4 import (
5 "fmt"
6 "strings"
7
8 "github.com/koment-dev/koment/internal/api"
9 )
10
11 // APIVersion is matched exactly. A record carrying anything else is refused
12 // rather than guessed at.
13 const APIVersion = api.Version
14
15 // KindAnnotation is the resource kind of an annotation record.
16 const KindAnnotation = "Annotation"
17
18 // TitleLimit keeps a title short enough to render beside code without being
19 // shortened, which is the only reason it exists (ADR 0115).
20 const TitleLimit = 72
21
22 const SchemaURL = api.SchemaBase + "annotation.schema.json"
23
24 // Type is the category of rationale a record carries.
25 type Type string
26
27 const (
28 TypeWhy Type = "why"
29 TypeGotcha Type = "gotcha"
30 TypeInvariant Type = "invariant"
31 TypeAntiPattern Type = "anti-pattern"
32 )
33
34 var Types = []Type{TypeWhy, TypeGotcha, TypeInvariant, TypeAntiPattern}
35
36 func ParseType(text string) (Type, error) {
37 for _, candidate := range Types {
38 if Type(text) == candidate {
39 return candidate, nil
40 }
41 }
42 return "", fmt.Errorf("unknown type %q, want one of %s", text, joinTypes())
43 }
44
45 func joinTypes() string {
46 names := make([]string, len(Types))
47 for index, annotationType := range Types {
48 names[index] = string(annotationType)
49 }
50 return strings.Join(names, ", ")
51 }
52
53 // AnchorStatus is the verdict of resolving an anchor against a file. It is
54 // computed by reading the file, never by trusting a stored value.
55 type AnchorStatus string
56
57 const (
58 AnchorOK AnchorStatus = "ok"
59 AnchorAmbiguous AnchorStatus = "ambiguous"
60 AnchorDrifted AnchorStatus = "drifted"
61 AnchorOrphaned AnchorStatus = "orphaned"
62 )
63
64 var AnchorStatuses = []AnchorStatus{AnchorOK, AnchorAmbiguous, AnchorDrifted, AnchorOrphaned}
65
66 func (s AnchorStatus) IsFailure() bool {
67 return s == AnchorAmbiguous || s == AnchorDrifted || s == AnchorOrphaned
68 }
69
70 func ParseAnchorStatus(text string) (AnchorStatus, error) {
71 for _, candidate := range AnchorStatuses {
72 if AnchorStatus(text) == candidate {
73 return candidate, nil
74 }
75 }
76 return "", fmt.Errorf("unknown resolution %q", text)
77 }
78
79 // Annotation is one rationale record on disk.
80 type Annotation struct {
81 APIVersion string `yaml:"apiVersion"`
82 Kind string `yaml:"kind"`
83 Metadata Metadata `yaml:"metadata"`
84 Spec Spec `yaml:"spec"`
85 Status Status `yaml:"status,omitempty"`
86 }
87
88 // Metadata identifies the record. Kubernetes names this field name; a ULID is
89 // not a DNS-1123 name, so koment diverges deliberately and calls it id. Do not
90 // align it without a migration.
91 type Metadata struct {
92 ID string `yaml:"id"`
93 Created Timestamp `yaml:"created"`
94 }
95
96 // Target is what the annotation is about. It is a mapping rather than a bare
97 // path so that a function or a member can join the file without reshaping the
98 // record again.
99 type Target struct {
100 File string `yaml:"file"`
101 }
102
103 // Spec is the authored intent: everything a person or agent decided.
104 type Spec struct {
105 Target Target `yaml:"target"`
106 Type Type `yaml:"type"`
107 Title string `yaml:"title,omitempty"`
108 Body string `yaml:"body"`
109 Anchor Anchor `yaml:"anchor"`
110 Author Author `yaml:"author"`
111 Git *GitContext `yaml:"git,omitempty"`
112 Policy *Policy `yaml:"policy,omitempty"`
113 }
114
115 // Status is what the last write observed, stamped with the commit it observed
116 // it at. Nothing reads it back as a verdict: a reader resolves the anchor
117 // against the file in front of it, and ResolvedCommit is what lets that reader
118 // see how old the recorded observation is.
119 type Status struct {
120 LastSeenLine int `yaml:"lastSeenLine,omitempty"`
121 Resolution AnchorStatus `yaml:"resolution,omitempty"`
122 ResolvedAt Timestamp `yaml:"resolvedAt,omitempty"`
123 ResolvedCommit string `yaml:"resolvedCommit,omitempty"`
124 }
125
126 // Observe records a resolution, leaving ResolvedAt alone when the verdict and
127 // the commit are already the ones recorded. ResolvedAt then answers "since
128 // when has this been true" instead of "when did a command last run".
129 func (s *Status) Observe(resolution AnchorStatus, commit string, at Timestamp) {
130 if s.Resolution == resolution && s.ResolvedCommit == commit {
131 return
132 }
133 s.Resolution = resolution
134 s.ResolvedCommit = commit
135 s.ResolvedAt = at
136 }
137
138 func (s Status) Validate(id string, scope Scope) error {
139 switch scope {
140 case ScopeFile:
141 if s.LastSeenLine != 0 {
142 return fmt.Errorf("annotation %s: a file-scoped record has no line to observe", id)
143 }
144 case ScopeExcerpt:
145 if s.LastSeenLine < 1 {
146 return fmt.Errorf("annotation %s: status.lastSeenLine %d is not a positive line number", id, s.LastSeenLine)
147 }
148 }
149 if s.Resolution != "" {
150 if _, err := ParseAnchorStatus(string(s.Resolution)); err != nil {
151 return fmt.Errorf("annotation %s: %w", id, err)
152 }
153 }
154 if (s.Resolution == "") != s.ResolvedAt.IsZero() {
155 return fmt.Errorf("annotation %s: status.resolution and status.resolvedAt are recorded together or not at all", id)
156 }
157 if s.ResolvedCommit != "" && !fullCommitSHA.MatchString(s.ResolvedCommit) {
158 return fmt.Errorf("annotation %s: status.resolvedCommit %q is not a full SHA", id, s.ResolvedCommit)
159 }
160 return nil
161 }
162
163 type Policy struct {
164 Exception string `yaml:"exception"`
165 Acknowledged bool `yaml:"acknowledged"`
166 }
167
168 func (p Policy) Validate(annotation Annotation) error {
169 if p.Exception != "inline-comment" || !p.Acknowledged {
170 return fmt.Errorf("annotation %s: policy must explicitly acknowledge an inline-comment exception", annotation.Metadata.ID)
171 }
172 if annotation.Spec.Type != TypeWhy || annotation.Spec.Anchor.Scope != ScopeExcerpt {
173 return fmt.Errorf("annotation %s: inline-comment policy requires a why annotation with an excerpt anchor", annotation.Metadata.ID)
174 }
175 return nil
176 }
177
178 // Headline is what a reader sees beside the code. A record written before
179 // titles existed still has to show something, so the first sentence of the body
180 // stands in, shortened at a word boundary. It is never written back: a derived
181 // title in the record would become a second copy of the body that drifts.
182 func (a Annotation) Headline() string {
183 if title := strings.TrimSpace(a.Spec.Title); title != "" {
184 return title
185 }
186 return shorten(firstSentence(a.Spec.Body), TitleLimit)
187 }
188
189 func firstSentence(body string) string {
190 flattened := strings.Join(strings.Fields(body), " ")
191 for index, character := range flattened {
192 if character != '.' && character != '!' && character != '?' {
193 continue
194 }
195 if index+1 >= len(flattened) || flattened[index+1] == ' ' {
196 return flattened[:index]
197 }
198 }
199 return flattened
200 }
201
202 func shorten(text string, limit int) string {
203 if len([]rune(text)) <= limit {
204 return text
205 }
206 runes := []rune(text)[:limit]
207 if space := strings.LastIndex(string(runes), " "); space > limit/2 {
208 return strings.TrimRight(string(runes)[:space], " ,;:") + "…"
209 }
210 return strings.TrimRight(string(runes), " ,;:") + "…"
211 }
212
213 func validTitle(id, title string) error {
214 if title == "" {
215 return nil
216 }
217 if strings.TrimSpace(title) == "" {
218 return fmt.Errorf("annotation %s: blank title", id)
219 }
220 if strings.ContainsAny(title, "\n\r") {
221 return fmt.Errorf("annotation %s: a title is one line", id)
222 }
223 if count := len([]rune(title)); count > TitleLimit {
224 return fmt.Errorf("annotation %s: title is %d characters, the limit is %d so it never needs shortening", id, count, TitleLimit)
225 }
226 return nil
227 }
228
229 func (a Annotation) Validate() error {
230 if a.APIVersion != APIVersion {
231 return fmt.Errorf("annotation %s has apiVersion %q, want %q", a.Metadata.ID, a.APIVersion, APIVersion)
232 }
233 if a.Kind != KindAnnotation {
234 return fmt.Errorf("annotation %s has kind %q, want %q", a.Metadata.ID, a.Kind, KindAnnotation)
235 }
236 if err := a.Metadata.Validate(); err != nil {
237 return err
238 }
239 if err := a.Spec.Validate(a.Metadata.ID); err != nil {
240 return err
241 }
242 if a.Spec.Policy != nil {
243 if err := a.Spec.Policy.Validate(a); err != nil {
244 return err
245 }
246 }
247 return a.Status.Validate(a.Metadata.ID, a.Spec.Anchor.Scope)
248 }
249
250 func (m Metadata) Validate() error {
251 if !ValidID(m.ID) {
252 return fmt.Errorf("annotation id %q is not a canonical ULID", m.ID)
253 }
254 if m.Created.IsZero() {
255 return fmt.Errorf("annotation %s: missing metadata.created", m.ID)
256 }
257 return nil
258 }
259
260 func (s Spec) Validate(id string) error {
261 if _, err := validSourcePath(s.Target.File); err != nil {
262 return fmt.Errorf("annotation %s target.file: %w", id, err)
263 }
264 if _, err := ParseType(string(s.Type)); err != nil {
265 return fmt.Errorf("annotation %s: %w", id, err)
266 }
267 if err := validTitle(id, s.Title); err != nil {
268 return err
269 }
270 if strings.TrimSpace(s.Body) == "" {
271 return fmt.Errorf("annotation %s: empty body", id)
272 }
273 if err := s.Anchor.Validate(id); err != nil {
274 return err
275 }
276 if s.Git != nil {
277 if err := s.Git.Validate(); err != nil {
278 return fmt.Errorf("annotation %s: %w", id, err)
279 }
280 }
281 if err := s.Author.Validate(); err != nil {
282 return fmt.Errorf("annotation %s: %w", id, err)
283 }
284 return nil
285 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close