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