Skip to main content

speccoder_core/
context.rs

1//! What reached a model call (constitution VIII): one manifest of every
2//! part handed to it — its kind, where it came from, its digest and size —
3//! journaled with the call and named in the evidence. Producers add
4//! entries through the runtime's one builder; nothing hashes or labels
5//! context itself.
6
7use serde::{Deserialize, Serialize};
8
9/// What a part is. A new kind is a new variant, never a free string.
10#[derive(
11    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
12)]
13#[serde(rename_all = "snake_case")]
14pub enum ContextKind {
15    /// The project's rules.
16    Constitution,
17    /// The unit's plan.
18    Plan,
19    /// The requirement's text (a bug's triad).
20    Story,
21    /// The task.
22    Task,
23    /// A person's rejection reason.
24    Rejection,
25    /// The verifier's digest of an earlier attempt.
26    RetryDigest,
27    /// A GitHub issue's title and body (untrusted).
28    Issue,
29    /// A requirement's acceptance scenario.
30    Scenario,
31    /// A person's note to an author call (*Ask Again*, a reading).
32    Note,
33    /// A person's review comment, with its anchor's quote.
34    Comment,
35}
36
37/// One part handed to a call.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
39pub struct ContextEntry {
40    /// What it is.
41    pub kind: ContextKind,
42    /// Where it came from: `person`, `worktree`, `speccoder://<producer>/<name>`,
43    /// `github:<owner>/<repo>#<n>`.
44    pub source: String,
45    /// A path, an id or a part pointer (`/bundle/plan`).
46    pub reference: String,
47    /// Hex SHA-256 of the exact bytes given.
48    pub sha256: String,
49    /// Their size.
50    pub bytes: u64,
51    /// Cut to a budget.
52    pub cut: bool,
53}
54
55/// Every part of one call, sorted by kind and reference.
56#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
57pub struct ContextManifest {
58    /// The parts.
59    pub entries: Vec<ContextEntry>,
60    /// Hex SHA-256 of [`ContextManifest::canonical`].
61    pub digest: String,
62}
63
64impl ContextManifest {
65    /// The entries' canonical JSON (sorted): what the digest is over.
66    #[must_use]
67    pub fn canonical(entries: &[ContextEntry]) -> String {
68        let mut sorted = entries.to_vec();
69        sorted.sort_by(|a, b| (a.kind, &a.reference).cmp(&(b.kind, &b.reference)));
70        serde_json::to_string(&sorted).unwrap_or_default()
71    }
72
73    /// The entries, sorted, with the digest `hash` gives over them.
74    #[must_use]
75    pub fn of(mut entries: Vec<ContextEntry>, hash: impl Fn(&[u8]) -> String) -> Self {
76        let digest = hash(Self::canonical(&entries).as_bytes());
77        entries.sort_by(|a, b| (a.kind, &a.reference).cmp(&(b.kind, &b.reference)));
78        Self { entries, digest }
79    }
80
81    /// The entries of `kind`.
82    pub fn of_kind(&self, kind: ContextKind) -> impl Iterator<Item = &ContextEntry> {
83        self.entries.iter().filter(move |e| e.kind == kind)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    fn entry(kind: ContextKind, reference: &str) -> ContextEntry {
92        ContextEntry {
93            kind,
94            source: "person".to_owned(),
95            reference: reference.to_owned(),
96            sha256: "00".to_owned(),
97            bytes: 2,
98            cut: false,
99        }
100    }
101
102    #[test]
103    fn the_digest_does_not_depend_on_the_order_parts_were_added() {
104        let h = |b: &[u8]| format!("{}", b.len());
105        let a = ContextManifest::of(
106            vec![
107                entry(ContextKind::Issue, "#42"),
108                entry(ContextKind::Story, "/triad"),
109            ],
110            h,
111        );
112        let b = ContextManifest::of(
113            vec![
114                entry(ContextKind::Story, "/triad"),
115                entry(ContextKind::Issue, "#42"),
116            ],
117            h,
118        );
119        assert_eq!(a, b);
120        assert_eq!(a.entries[0].kind, ContextKind::Story);
121        assert_eq!(a.of_kind(ContextKind::Issue).count(), 1);
122    }
123}