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    io::Writer,
7};
8use crate::object::{ChangeLineageKind, State};
9
10pub(super) const STATE_MAGIC: &[u8; 4] = b"HCS1";
11
12/// Whether `bytes` begin with the compact-state frame discriminator.
13pub fn is_state_frame(bytes: &[u8]) -> bool {
14    bytes.starts_with(STATE_MAGIC)
15}
16
17/// Encode states as lossless columns, omitting only the derivable state id.
18pub 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}