heddle_object_model/compact/
state.rs1use super::{
4 Result,
5 dictionary::{PrincipalKey, StateDictionaries},
6 invalid,
7 io::Writer,
8 limits::MAX_COMPACT_STATE_COUNT,
9};
10use crate::object::{ChangeLineageKind, State};
11
12pub(super) const STATE_MAGIC_V1: &[u8; 4] = b"HCS1";
13pub(super) const STATE_MAGIC: &[u8; 4] = b"HCS2";
14
15pub fn is_state_frame(bytes: &[u8]) -> bool {
17 bytes.starts_with(STATE_MAGIC) || bytes.starts_with(STATE_MAGIC_V1)
18}
19
20pub fn encode_state_frame(states: &[State]) -> Result<Vec<u8>> {
26 encode_state_frame_versioned(states, STATE_MAGIC)
27}
28
29#[cfg(test)]
30pub(super) fn encode_state_frame_hcs1(states: &[State]) -> Result<Vec<u8>> {
31 encode_state_frame_versioned(states, STATE_MAGIC_V1)
32}
33
34fn encode_state_frame_versioned(states: &[State], magic: &[u8; 4]) -> Result<Vec<u8>> {
35 if states.len() > MAX_COMPACT_STATE_COUNT {
36 return Err(invalid(format!(
37 "state frame count {} exceeds maximum {MAX_COMPACT_STATE_COUNT}",
38 states.len()
39 )));
40 }
41 let dictionaries = StateDictionaries::from_states(states);
42 let mut output = Writer::new(magic);
43 output.put_u64(states.len() as u64);
44 encode_dictionaries(&mut output, &dictionaries, magic == STATE_MAGIC);
45 encode_structure(&mut output, states);
46 encode_attribution(&mut output, states, &dictionaries);
47 encode_intent_and_verification(&mut output, states)?;
48 encode_timestamps(&mut output, states);
49 encode_fidelity(&mut output, states, &dictionaries);
50 encode_lineage(&mut output, states);
51 Ok(output.finish())
52}
53
54fn encode_structure(output: &mut Writer, states: &[State]) {
55 for state in states {
56 output.put_fixed(state.change_id.as_bytes());
57 }
58 for state in states {
59 output.put_fixed(state.tree.as_bytes());
60 }
61 for state in states {
62 output.put_u64(state.parents.len() as u64);
63 for parent in &state.parents {
64 output.put_fixed(parent.as_bytes());
65 }
66 }
67}
68
69fn encode_attribution(output: &mut Writer, states: &[State], dictionaries: &StateDictionaries) {
70 for state in states {
71 output.put_u64(dictionaries.principal_index(&state.attribution.principal));
72 }
73 for state in states {
74 output.put_u64(
75 state
76 .attribution
77 .agent
78 .as_ref()
79 .map(|agent| dictionaries.agent_index(agent) + 1)
80 .unwrap_or(0),
81 );
82 }
83}
84
85fn encode_intent_and_verification(output: &mut Writer, states: &[State]) -> Result<()> {
86 for state in states {
87 output.put_optional_bytes(state.intent.as_deref().map(str::as_bytes));
88 }
89 for state in states {
90 put_optional_f32(output, state.confidence);
91 }
92 for state in states {
93 encode_verification(output, state)?;
94 }
95 for state in states {
96 output.put_u8(state.status.to_byte());
97 }
98 Ok(())
99}
100
101fn encode_timestamps(output: &mut Writer, states: &[State]) {
102 let mut previous = 0i64;
103 for (index, state) in states.iter().enumerate() {
104 let seconds = state.created_at.timestamp();
105 output.put_i64(if index == 0 {
106 seconds
107 } else {
108 seconds - previous
109 });
110 output.put_u64(u64::from(state.created_at.timestamp_subsec_nanos()));
111 previous = seconds;
112 }
113 for state in states {
114 match state.authored_at {
115 Some(timestamp) => {
116 output.put_u8(1);
117 output.put_i64(timestamp.timestamp() - state.created_at.timestamp());
118 output.put_u64(u64::from(timestamp.timestamp_subsec_nanos()));
119 }
120 None => output.put_u8(0),
121 }
122 }
123 for state in states {
124 output.put_i64(i64::from(state.authored_tz_offset));
125 output.put_i64(i64::from(state.committer_tz_offset));
126 }
127}
128
129fn encode_fidelity(output: &mut Writer, states: &[State], dictionaries: &StateDictionaries) {
130 for state in states {
131 match state.provenance {
132 Some(hash) => {
133 output.put_u8(1);
134 output.put_fixed(hash.as_bytes());
135 }
136 None => output.put_u8(0),
137 }
138 }
139 for state in states {
140 output.put_u64(
141 state
142 .committer
143 .as_ref()
144 .map(|value| dictionaries.principal_index(value) + 1)
145 .unwrap_or(0),
146 );
147 }
148 for state in states {
149 output.put_optional_bytes(state.raw_message.as_deref());
150 }
151 for state in states {
152 output.put_u64(state.extra_headers.len() as u64);
153 for (name, value) in &state.extra_headers {
154 output.put_bytes(name);
155 output.put_bytes(value);
156 }
157 }
158 for state in states {
159 output.put_bool(state.git_lossy);
160 }
161}
162
163fn encode_lineage(output: &mut Writer, states: &[State]) {
164 for state in states {
165 output.put_u64(state.lineage.len() as u64);
166 for lineage in &state.lineage {
167 output.put_u8(lineage_kind_tag(lineage.kind));
168 output.put_fixed(lineage.source_change.as_bytes());
169 output.put_fixed(lineage.source_state.as_bytes());
170 }
171 }
172}
173
174fn encode_verification(output: &mut Writer, state: &State) -> Result<()> {
175 let Some(value) = &state.verification else {
176 output.put_u8(0);
177 return Ok(());
178 };
179 output.put_u8(1);
180 put_optional_bool(output, value.tests_passed);
181 put_optional_u32(output, value.tests_failed);
182 put_optional_f32(output, value.coverage_pct);
183 put_optional_f32(output, value.coverage_delta);
184 put_optional_u32(output, value.lint_warnings);
185 output.put_u64(value.custom.len() as u64);
186 for (key, json) in &value.custom {
187 output.put_bytes(key.as_bytes());
188 output.put_bytes(&rmp_serde::to_vec(json)?);
189 }
190 Ok(())
191}
192
193fn encode_dictionaries(
194 output: &mut Writer,
195 dictionaries: &StateDictionaries,
196 include_cursor_fields: bool,
197) {
198 output.put_u64(dictionaries.principals.len() as u64);
199 for PrincipalKey(name, email) in &dictionaries.principals {
200 output.put_bytes(name);
201 output.put_bytes(email);
202 }
203 output.put_u64(dictionaries.agents.len() as u64);
204 for agent in &dictionaries.agents {
205 output.put_bytes(&agent.provider);
206 output.put_bytes(&agent.model);
207 output.put_optional_bytes(agent.session_id.as_deref());
208 output.put_optional_bytes(agent.segment_id.as_deref());
209 output.put_optional_bytes(agent.policy_id.as_deref());
210 if include_cursor_fields {
211 output.put_optional_bytes(agent.thought_level.as_deref());
212 output.put_optional_bytes(agent.parent.as_deref());
213 }
214 }
215}
216
217fn put_optional_bool(output: &mut Writer, value: Option<bool>) {
218 output.put_u8(match value {
219 None => 0,
220 Some(false) => 1,
221 Some(true) => 2,
222 });
223}
224
225fn put_optional_u32(output: &mut Writer, value: Option<u32>) {
226 output.put_u64(value.map(u64::from).map(|value| value + 1).unwrap_or(0));
227}
228
229fn put_optional_f32(output: &mut Writer, value: Option<f32>) {
230 match value {
231 Some(value) => {
232 output.put_u8(1);
233 output.put_fixed(&value.to_le_bytes());
234 }
235 None => output.put_u8(0),
236 }
237}
238
239pub(super) fn lineage_kind_tag(kind: ChangeLineageKind) -> u8 {
240 match kind {
241 ChangeLineageKind::CherryPick => 1,
242 ChangeLineageKind::Collapse => 2,
243 ChangeLineageKind::Revert => 3,
244 ChangeLineageKind::GitProjection => 4,
245 }
246}