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