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