返回 DeepSeek-Reasonix
render.go
根目录 / internal / memory / render.go
1 // Frontmatter serialization for saved facts: the YAML shape render emits and
2 // the legacy type-routing compatibility it preserves.
3 package memory
4
5 import (
6 "strings"
7 "time"
8
9 "gopkg.in/yaml.v3"
10 )
11
12 // memoryFrontmatter is the YAML shape render emits, mirroring the auto-memory
13 // shape (name / description / metadata.type) so the files are interchangeable
14 // with that ecosystem and re-readable by loadMemory. Marshaled by yaml.v3 so a
15 // value containing ": ", '#', or quotes is escaped instead of corrupting the
16 // block; plain values render byte-identically to the previous hand-built form.
17 type memoryFrontmatter struct {
18 ID string `yaml:"id,omitempty"`
19 Revision int `yaml:"revision,omitempty"`
20 CreatedAt string `yaml:"created_at,omitempty"`
21 UpdatedAt string `yaml:"updated_at,omitempty"`
22 Name string `yaml:"name"`
23 Title string `yaml:"title,omitempty"`
24 Desc string `yaml:"description"`
25 Keywords string `yaml:"keywords,omitempty"`
26 Activation string `yaml:"activation,omitempty"`
27 Volatility string `yaml:"volatility,omitempty"`
28 SubjectKey string `yaml:"subject_key,omitempty"`
29 ExpiresAt string `yaml:"expires_at,omitempty"`
30 LastVerifiedAt string `yaml:"last_verified_at,omitempty"`
31 Metadata struct {
32 Type string `yaml:"type"`
33 FactType string `yaml:"fact_type,omitempty"`
34 Scope string `yaml:"scope"`
35 } `yaml:"metadata"`
36 }
37
38 // render serializes a memory to frontmatter + body.
39 func render(m Memory, name string) string {
40 fm := memoryFrontmatter{
41 ID: m.ID, Revision: m.Revision, Name: name, Title: oneLine(m.Title), Desc: oneLine(m.Description),
42 Keywords: oneLine(m.Keywords), Activation: string(NormalizeActivation(string(m.Activation))),
43 Volatility: string(NormalizeVolatility(string(m.Volatility))),
44 SubjectKey: NormalizeSubjectKey(m.SubjectKey),
45 }
46 if !m.CreatedAt.IsZero() {
47 fm.CreatedAt = m.CreatedAt.UTC().Format(time.RFC3339Nano)
48 }
49 if !m.UpdatedAt.IsZero() {
50 fm.UpdatedAt = m.UpdatedAt.UTC().Format(time.RFC3339Nano)
51 }
52 if !m.ExpiresAt.IsZero() {
53 fm.ExpiresAt = m.ExpiresAt.UTC().Format(time.RFC3339Nano)
54 }
55 if !m.LastVerifiedAt.IsZero() {
56 fm.LastVerifiedAt = m.LastVerifiedAt.UTC().Format(time.RFC3339Nano)
57 }
58 actualType := NormalizeType(string(m.Type))
59 scope := NormalizeFactScope(string(m.Scope))
60 compatType := previousReleaseRoutingType(actualType, scope)
61 fm.Metadata.Type = string(compatType)
62 if compatType != actualType {
63 fm.Metadata.FactType = string(actualType)
64 }
65 fm.Metadata.Scope = string(scope)
66 var b strings.Builder
67 b.WriteString("---\n")
68 enc := yaml.NewEncoder(&b)
69 enc.SetIndent(2)
70 // Encoding a flat struct of strings cannot fail.
71 _ = enc.Encode(fm)
72 _ = enc.Close()
73 b.WriteString("---\n\n")
74 b.WriteString(strings.TrimSpace(m.Body))
75 b.WriteString("\n")
76 return b.String()
77 }
78
79 // previousReleaseRoutingType keeps scope safe when an older Reasonix binary
80 // shares the same state directory. Previous releases routed user/feedback to
81 // GlobalDir and project/reference to Dir, so metadata.type remains a compatible
82 // routing hint while metadata.fact_type preserves the independent new category.
83 func previousReleaseRoutingType(actual Type, scope FactScope) Type {
84 if scope == FactScopeGlobal {
85 if actual == TypeUser || actual == TypeFeedback {
86 return actual
87 }
88 return TypeUser
89 }
90 if actual == TypeProject || actual == TypeReference {
91 return actual
92 }
93 return TypeProject
94 }
95
95 lines GO