Forge
csharpdeeb25a2
1using System.Text.Json;
2
3namespace CasaField.Core;
4
5public sealed record FieldGrid(
6 int Width,
7 int Height,
8 IReadOnlyList<IReadOnlyList<string>> Cells,
9 string? SourceBundle,
10 IReadOnlyList<string>? SourceBundles,
11 string Codec,
12 bool Union);
13
14public sealed record FieldStateSnapshot(
15 int SchemaVersion,
16 int FieldVersion,
17 bool Stale,
18 FieldGrid? Grid,
19 IReadOnlyList<ClaimNavItem> ClaimsNav,
20 IReadOnlyDictionary<string, JsonElement>? BundlesMeta);
21
22public sealed record ClaimNavItem(
23 string ConceptId,
24 string DocPath,
25 string Section,
26 string Text,
27 string? Bundle,
28 string? ClaimKind,
29 IReadOnlyList<CodeAnchorItem>? CodeAnchors);
30
31public sealed record CodeAnchorItem(string File, int? Line, int? LineEnd);
32
33public static class FieldStateLoader
34{
35 private static readonly JsonSerializerOptions JsonOptions = new()
36 {
37 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
38 PropertyNameCaseInsensitive = true,
39 };
40
41 public static FieldStateSnapshot Load(string fieldStatePath, string storeDirectory)
42 {
43 using var doc = JsonDocument.Parse(File.ReadAllText(fieldStatePath));
44 var root = doc.RootElement;
45 var schemaVersion = root.TryGetProperty("schema_version", out var sv) ? sv.GetInt32() : 1;
46 var fieldVersion = root.TryGetProperty("field_version", out var fv) ? fv.GetInt32() : 0;
47
48 FieldGrid? grid = null;
49 if (root.TryGetProperty("grid", out var gridEl) && gridEl.ValueKind == JsonValueKind.Object)
50 {
51 var cells = new List<IReadOnlyList<string>>();
52 if (gridEl.TryGetProperty("cells", out var cellsEl) && cellsEl.ValueKind == JsonValueKind.Array)
53 {
54 foreach (var cell in cellsEl.EnumerateArray())
55 {
56 var tokens = new List<string>();
57 if (cell.ValueKind == JsonValueKind.Array)
58 {
59 foreach (var t in cell.EnumerateArray())
60 {
61 if (t.ValueKind == JsonValueKind.String)
62 tokens.Add(t.GetString() ?? "");
63 }
64 }
65 cells.Add(tokens);
66 }
67 }
68
69 var sourceBundles = new List<string>();
70 if (gridEl.TryGetProperty("source_bundles", out var sbArr) && sbArr.ValueKind == JsonValueKind.Array)
71 {
72 foreach (var b in sbArr.EnumerateArray())
73 {
74 if (b.ValueKind == JsonValueKind.String)
75 sourceBundles.Add(b.GetString() ?? "");
76 }
77 }
78
79 grid = new FieldGrid(
80 gridEl.TryGetProperty("width", out var w) ? w.GetInt32() : 64,
81 gridEl.TryGetProperty("height", out var h) ? h.GetInt32() : 64,
82 cells,
83 gridEl.TryGetProperty("source_bundle", out var sb) ? sb.GetString() : null,
84 sourceBundles.Count > 0 ? sourceBundles : null,
85 gridEl.TryGetProperty("codec", out var c) ? c.GetString() ?? "v3" : "v3",
86 gridEl.TryGetProperty("union", out var u) && u.ValueKind == JsonValueKind.True);
87 }
88
89 var nav = ReadClaimsNav(root);
90 var stale = IsStale(root, storeDirectory);
91 Dictionary<string, JsonElement>? bundlesMeta = null;
92 if (root.TryGetProperty("bundles", out var bundles) && bundles.ValueKind == JsonValueKind.Object)
93 {
94 bundlesMeta = bundles.EnumerateObject().ToDictionary(p => p.Name, p => p.Value);
95 }
96
97 return new FieldStateSnapshot(schemaVersion, fieldVersion, stale, grid, nav, bundlesMeta);
98 }
99
100 private static List<ClaimNavItem> ReadClaimsNav(JsonElement root)
101 {
102 if (!root.TryGetProperty("claims_nav", out var nav) || nav.ValueKind != JsonValueKind.Array)
103 return [];
104
105 var list = new List<ClaimNavItem>();
106 foreach (var item in nav.EnumerateArray())
107 {
108 var cid = item.TryGetProperty("concept_id", out var c) ? c.GetString() : null;
109 var dp = item.TryGetProperty("doc_path", out var d) ? d.GetString() : null;
110 if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(dp))
111 continue;
112
113 var sec = item.TryGetProperty("section", out var s) ? s.GetString() ?? "" : "";
114 var text = item.TryGetProperty("text", out var tx) ? tx.GetString() ?? "" : "";
115 var claimKind = item.TryGetProperty("claim_kind", out var ck) ? ck.GetString() : null;
116 var bundle = item.TryGetProperty("bundle", out var b) ? b.GetString() : null;
117 IReadOnlyList<CodeAnchorItem>? anchors = null;
118 if (item.TryGetProperty("code_anchors", out var ca) && ca.ValueKind == JsonValueKind.Array)
119 {
120 var al = new List<CodeAnchorItem>();
121 foreach (var a in ca.EnumerateArray())
122 {
123 var file = a.TryGetProperty("file", out var f) ? f.GetString() : null;
124 if (string.IsNullOrWhiteSpace(file))
125 continue;
126 int? line = a.TryGetProperty("line", out var ln) && ln.ValueKind == JsonValueKind.Number ? ln.GetInt32() : null;
127 int? lineEnd = a.TryGetProperty("line_end", out var le) && le.ValueKind == JsonValueKind.Number ? le.GetInt32() : null;
128 al.Add(new CodeAnchorItem(file, line, lineEnd));
129 }
130 if (al.Count > 0)
131 anchors = al;
132 }
133
134 list.Add(new ClaimNavItem(cid!, dp!, sec, text, bundle, claimKind, anchors));
135 }
136
137 return list;
138 }
139
140 private static bool IsStale(JsonElement root, string storeDirectory)
141 {
142 if (!root.TryGetProperty("bundles", out var bundles) || bundles.ValueKind != JsonValueKind.Object)
143 return false;
144
145 foreach (var prop in bundles.EnumerateObject())
146 {
147 if (!File.Exists(Path.Combine(storeDirectory, "bundles", prop.Name)))
148 return true;
149 }
150
151 if (!root.TryGetProperty("fingerprint", out var fp) || !fp.TryGetProperty("bundles", out var fpBundles))
152 return false;
153
154 foreach (var prop in fpBundles.EnumerateObject())
155 {
156 var path = Path.Combine(storeDirectory, "bundles", prop.Name);
157 if (!File.Exists(path))
158 return true;
159 var recorded = prop.Value.TryGetProperty("content_hash", out var h) ? h.GetString() : null;
160 if (string.IsNullOrEmpty(recorded))
161 continue;
162 if (BundleContentHash(File.ReadAllText(path)) != recorded)
163 return true;
164 }
165
166 return false;
167 }
168
169 internal static string BundleContentHash(string json)
170 {
171 using var doc = JsonDocument.Parse(json);
172 var canonical = CanonicalJsonString(doc.RootElement);
173 var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(canonical));
174 return Convert.ToHexString(bytes)[..16].ToLowerInvariant();
175 }
176
177 private static string CanonicalJsonString(JsonElement el)
178 {
179 if (el.ValueKind == JsonValueKind.Object)
180 {
181 var props = el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal).ToList();
182 var inner = string.Join(",", props.Select(p => $"\"{p.Name}\":{CanonicalJsonString(p.Value)}"));
183 return "{" + inner + "}";
184 }
185
186 if (el.ValueKind == JsonValueKind.Array)
187 {
188 var items = el.EnumerateArray().Select(CanonicalJsonString);
189 return "[" + string.Join(",", items) + "]";
190 }
191
192 return el.GetRawText();
193 }
194}
195
View only · write via MCP/CIDE