elixir: define model structs with YAML serialization

Swadhin Biswas committed 7 months ago
096b0b0
1 parent 778bec3
commit 096b0b0
1 file changed
+344 -0
Showing 1 changed file+344-0
▼ELIXIR/lib/contexa/models.ex
+344-0
1
+defmodule Contexa.Models do
2
+ @moduledoc """
3
+ Data structures for the contexa GCC workspace.
4
+
5
+ contexa — Git-inspired context management for LLM agents.
6
+ Based on arXiv:2508.00031.
7
+ """
8
+
9
+ # ── OTARecord ─────────────────────────────────────────────────────
10
+
11
+ defmodule OTARecord do
12
+ @moduledoc "An Observation-Thought-Action cycle record."
13
+ defstruct [:step, :timestamp, :observation, :thought, :action]
14
+
15
+ @type t :: %__MODULE__{
16
+ step: non_neg_integer(),
17
+ timestamp: String.t(),
18
+ observation: String.t(),
19
+ thought: String.t(),
20
+ action: String.t()
21
+ }
22
+ end
23
+
24
+ defmodule CommitRecord do
25
+ @moduledoc "A milestone checkpoint record."
26
+ defstruct [
27
+ :commit_id,
28
+ :branch_name,
29
+ :branch_purpose,
30
+ :previous_progress_summary,
31
+ :this_commit_contribution,
32
+ :timestamp
33
+ ]
34
+
35
+ @type t :: %__MODULE__{
36
+ commit_id: String.t(),
37
+ branch_name: String.t(),
38
+ branch_purpose: String.t(),
39
+ previous_progress_summary: String.t(),
40
+ this_commit_contribution: String.t(),
41
+ timestamp: String.t()
42
+ }
43
+ end
44
+
45
+ defmodule BranchMetadata do
46
+ @moduledoc "Branch metadata stored in metadata.yaml."
47
+ defstruct [
48
+ :name,
49
+ :purpose,
50
+ :created_from,
51
+ :created_at,
52
+ status: "active",
53
+ merged_into: nil,
54
+ merged_at: nil
55
+ ]
56
+
57
+ @type t :: %__MODULE__{
58
+ name: String.t(),
59
+ purpose: String.t(),
60
+ created_from: String.t(),
61
+ created_at: String.t(),
62
+ status: String.t(),
63
+ merged_into: String.t() | nil,
64
+ merged_at: String.t() | nil
65
+ }
66
+ end
67
+
68
+ defmodule ContextResult do
69
+ @moduledoc "Result of a CONTEXT query."
70
+ defstruct [
71
+ :branch_name,
72
+ :k,
73
+ commits: [],
74
+ ota_records: [],
75
+ main_roadmap: "",
76
+ metadata: nil
77
+ ]
78
+
79
+ @type t :: %__MODULE__{
80
+ branch_name: String.t(),
81
+ k: non_neg_integer(),
82
+ commits: [CommitRecord.t()],
83
+ ota_records: [OTARecord.t()],
84
+ main_roadmap: String.t(),
85
+ metadata: BranchMetadata.t() | nil
86
+ }
87
+ end
88
+
89
+ # ── Constructors ──────────────────────────────────────────────────
90
+
91
+ @doc "Generate an 8-character hex ID."
92
+ @spec generate_id() :: String.t()
93
+ def generate_id do
94
+ :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
95
+ end
96
+
97
+ @doc "Get current UTC timestamp in ISO 8601 format."
98
+ @spec timestamp() :: String.t()
99
+ def timestamp do
100
+ DateTime.utc_now() |> DateTime.to_iso8601()
101
+ end
102
+
103
+ @doc "Create a new OTARecord."
104
+ @spec new_ota(non_neg_integer(), String.t(), String.t(), String.t()) :: OTARecord.t()
105
+ def new_ota(step, observation, thought, action) do
106
+ %OTARecord{
107
+ step: step,
108
+ timestamp: timestamp(),
109
+ observation: observation || "",
110
+ thought: thought || "",
111
+ action: action || ""
112
+ }
113
+ end
114
+
115
+ @doc "Create a new CommitRecord."
116
+ @spec new_commit(String.t(), String.t(), String.t(), String.t()) :: CommitRecord.t()
117
+ def new_commit(branch_name, branch_purpose, previous_summary, contribution) do
118
+ %CommitRecord{
119
+ commit_id: generate_id(),
120
+ branch_name: branch_name,
121
+ branch_purpose: branch_purpose || "",
122
+ previous_progress_summary: previous_summary || "",
123
+ this_commit_contribution: contribution || "",
124
+ timestamp: timestamp()
125
+ }
126
+ end
127
+
128
+ @doc "Create a new BranchMetadata."
129
+ @spec new_branch_metadata(String.t(), String.t(), String.t()) :: BranchMetadata.t()
130
+ def new_branch_metadata(name, purpose, created_from \\ "") do
131
+ %BranchMetadata{
132
+ name: name,
133
+ purpose: purpose,
134
+ created_from: created_from,
135
+ created_at: timestamp(),
136
+ status: "active",
137
+ merged_into: nil,
138
+ merged_at: nil
139
+ }
140
+ end
141
+
142
+ # ── Markdown serialization ────────────────────────────────────────
143
+
144
+ @doc "Render an OTARecord as markdown."
145
+ @spec ota_to_markdown(OTARecord.t()) :: String.t()
146
+ def ota_to_markdown(%OTARecord{} = rec) do
147
+ """
148
+ ### Step #{rec.step} — #{rec.timestamp}
149
+ **Observation:** #{rec.observation}
150
+
151
+ **Thought:** #{rec.thought}
152
+
153
+ **Action:** #{rec.action}
154
+
155
+ ---
156
+ """
157
+ end
158
+
159
+ @doc "Render a CommitRecord as markdown."
160
+ @spec commit_to_markdown(CommitRecord.t()) :: String.t()
161
+ def commit_to_markdown(%CommitRecord{} = rec) do
162
+ """
163
+ ## Commit `#{rec.commit_id}`
164
+ **Timestamp:** #{rec.timestamp}
165
+
166
+ **Branch Purpose:** #{rec.branch_purpose}
167
+
168
+ **Previous Progress Summary:** #{rec.previous_progress_summary}
169
+
170
+ **This Commit's Contribution:** #{rec.this_commit_contribution}
171
+
172
+ ---
173
+ """
174
+ end
175
+
176
+ # ── YAML serialization ───────────────────────────────────────────
177
+
178
+ @doc "Serialize BranchMetadata to YAML string."
179
+ @spec metadata_to_yaml(BranchMetadata.t()) :: String.t()
180
+ def metadata_to_yaml(%BranchMetadata{} = meta) do
181
+ yaml_val = fn
182
+ nil -> "null"
183
+ "" -> "''"
184
+ v when is_binary(v) ->
185
+ if String.contains?(v, ":") or String.contains?(v, "#") do
186
+ "'#{String.replace(v, "'", "''")}'"
187
+ else
188
+ v
189
+ end
190
+ end
191
+
192
+ # Keys in sorted order for cross-language compatibility
193
+ [
194
+ "created_at: #{yaml_val.(meta.created_at)}",
195
+ "created_from: #{yaml_val.(meta.created_from)}",
196
+ "merged_at: #{yaml_val.(meta.merged_at)}",
197
+ "merged_into: #{yaml_val.(meta.merged_into)}",
198
+ "name: #{yaml_val.(meta.name)}",
199
+ "purpose: #{yaml_val.(meta.purpose)}",
200
+ "status: #{yaml_val.(meta.status)}"
201
+ ]
202
+ |> Enum.join("\n")
203
+ |> Kernel.<>("\n")
204
+ end
205
+
206
+ @doc "Parse BranchMetadata from YAML string."
207
+ @spec metadata_from_yaml(String.t()) :: BranchMetadata.t()
208
+ def metadata_from_yaml(text) do
209
+ pairs =
210
+ text
211
+ |> String.split("\n", trim: true)
212
+ |> Enum.reduce(%{}, fn line, acc ->
213
+ case Regex.run(~r/^(\S+):\s*(.*)$/, line) do
214
+ [_, key, val] ->
215
+ val = val |> String.trim()
216
+ val = cond do
217
+ val == "null" -> nil
218
+ val == "''" -> ""
219
+ String.starts_with?(val, "'") and String.ends_with?(val, "'") ->
220
+ val |> String.slice(1..-2//1) |> String.replace("''", "'")
221
+ true -> val
222
+ end
223
+ Map.put(acc, key, val)
224
+ _ -> acc
225
+ end
226
+ end)
227
+
228
+ %BranchMetadata{
229
+ name: Map.get(pairs, "name", ""),
230
+ purpose: Map.get(pairs, "purpose", ""),
231
+ created_from: Map.get(pairs, "created_from", ""),
232
+ created_at: Map.get(pairs, "created_at", ""),
233
+ status: Map.get(pairs, "status", "active"),
234
+ merged_into: Map.get(pairs, "merged_into"),
235
+ merged_at: Map.get(pairs, "merged_at")
236
+ }
237
+ end
238
+
239
+ # ── Markdown parsing ──────────────────────────────────────────────
240
+
241
+ @doc "Parse commit records from commit.md content."
242
+ @spec parse_commits(String.t()) :: [CommitRecord.t()]
243
+ def parse_commits(text) do
244
+ text
245
+ |> String.split("\n---\n")
246
+ |> Enum.map(&String.trim/1)
247
+ |> Enum.filter(&(&1 != ""))
248
+ |> Enum.reduce([], fn block, acc ->
249
+ commit_id = case Regex.run(~r/## Commit `([^`]+)`/, block) do
250
+ [_, id] -> id
251
+ _ -> ""
252
+ end
253
+
254
+ if commit_id != "" do
255
+ extract = fn pattern ->
256
+ case Regex.run(pattern, block) do
257
+ [_, val] -> String.trim(val)
258
+ _ -> ""
259
+ end
260
+ end
261
+
262
+ rec = %CommitRecord{
263
+ commit_id: commit_id,
264
+ branch_name: "",
265
+ timestamp: extract.(~r/\*\*Timestamp:\*\*\s*(.+)/),
266
+ branch_purpose: extract.(~r/\*\*Branch Purpose:\*\*\s*(.+)/),
267
+ previous_progress_summary: extract.(~r/\*\*Previous Progress Summary:\*\*\s*(.+)/),
268
+ this_commit_contribution: extract.(~r/\*\*This Commit's Contribution:\*\*\s*(.+)/)
269
+ }
270
+ acc ++ [rec]
271
+ else
272
+ acc
273
+ end
274
+ end)
275
+ end
276
+
277
+ @doc "Parse OTA records from log.md content."
278
+ @spec parse_ota(String.t()) :: [OTARecord.t()]
279
+ def parse_ota(text) do
280
+ text
281
+ |> String.split("\n---\n")
282
+ |> Enum.map(&String.trim/1)
283
+ |> Enum.filter(&(&1 != ""))
284
+ |> Enum.reduce([], fn block, acc ->
285
+ {step, ts} = case Regex.run(~r/### Step (\d+)\s*[—-]\s*(.+)/, block) do
286
+ [_, s, t] -> {String.to_integer(s), String.trim(t)}
287
+ _ -> {0, ""}
288
+ end
289
+
290
+ extract = fn pattern ->
291
+ case Regex.run(pattern, block) do
292
+ [_, val] -> String.trim(val)
293
+ _ -> ""
294
+ end
295
+ end
296
+
297
+ obs = extract.(~r/\*\*Observation:\*\*\s*(.+)/)
298
+ thought = extract.(~r/\*\*Thought:\*\*\s*(.+)/)
299
+ action = extract.(~r/\*\*Action:\*\*\s*(.+)/)
300
+
301
+ if obs != "" or thought != "" or action != "" do
302
+ rec = %OTARecord{
303
+ step: step,
304
+ timestamp: ts,
305
+ observation: obs,
306
+ thought: thought,
307
+ action: action
308
+ }
309
+ acc ++ [rec]
310
+ else
311
+ acc
312
+ end
313
+ end)
314
+ end
315
+
316
+ # ── Context summary ──────────────────────────────────────────────
317
+
318
+ @doc "Render a ContextResult as a markdown summary."
319
+ @spec context_summary(ContextResult.t()) :: String.t()
320
+ def context_summary(%ContextResult{} = ctx) do
321
+ commit_text = ctx.commits
322
+ |> Enum.map(&commit_to_markdown/1)
323
+ |> Enum.join("\n")
324
+
325
+ total = length(ctx.ota_records)
326
+ recent_ota = ctx.ota_records |> Enum.take(-5)
327
+ ota_text = recent_ota
328
+ |> Enum.map(&ota_to_markdown/1)
329
+ |> Enum.join("\n")
330
+
331
+ """
332
+ # CONTEXT — branch `#{ctx.branch_name}` (K=#{ctx.k})
333
+
334
+ ## Global Roadmap
335
+ #{ctx.main_roadmap}
336
+
337
+ ## Last #{ctx.k} Commit(s)
338
+ #{commit_text}
339
+
340
+ ## Recent OTA steps (#{total} total)
341
+ #{ota_text}
342
+ """
343
+ end
344
+end
0
345