Skip to main content

heddle_object_model/compact/
state.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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
14/// Whether `bytes` begin with the compact-state frame discriminator.
15pub fn is_state_frame(bytes: &[u8]) -> bool {
16    bytes.starts_with(STATE_MAGIC)
17}
18
19/// Encode states as lossless columns, omitting only the derivable state id.
20pub 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}