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    /// The test layout speccoder detected from the checkout.
36    TestLayout,
37}
38
39/// One part handed to a call.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
41pub struct ContextEntry {
42    /// What it is.
43    pub kind: ContextKind,
44    /// Where it came from: `person`, `worktree`, `speccoder://<producer>/<name>`,
45    /// `github:<owner>/<repo>#<n>`.
46    pub source: String,
47    /// A path, an id or a part pointer (`/bundle/plan`).
48    pub reference: String,
49    /// Hex SHA-256 of the exact bytes given.
50    pub sha256: String,
51    /// Their size.
52    pub bytes: u64,
53    /// Cut to a budget.
54    pub cut: bool,
55}
56
57/// Every part of one call, sorted by kind and reference.
58#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
59pub struct ContextManifest {
60    /// The parts.
61    pub entries: Vec<ContextEntry>,
62    /// Hex SHA-256 of [`ContextManifest::canonical`].
63    pub digest: String,
64}
65
66impl ContextManifest {
67    /// The entries' canonical JSON (sorted): what the digest is over.
68    #[must_use]
69    pub fn canonical(entries: &[ContextEntry]) -> String {
70        let mut sorted = entries.to_vec();
71        sorted.sort_by(|a, b| (a.kind, &a.reference).cmp(&(b.kind, &b.reference)));
72        serde_json::to_string(&sorted).unwrap_or_default()
73    }
74
75    /// The entries, sorted, with the digest `hash` gives over them.
76    #[must_use]
77    pub fn of(mut entries: Vec<ContextEntry>, hash: impl Fn(&[u8]) -> String) -> Self {
78        let digest = hash(Self::canonical(&entries).as_bytes());
79        entries.sort_by(|a, b| (a.kind, &a.reference).cmp(&(b.kind, &b.reference)));
80        Self { entries, digest }
81    }
82
83    /// The entries of `kind`.
84    pub fn of_kind(&self, kind: ContextKind) -> impl Iterator<Item = &ContextEntry> {
85        self.entries.iter().filter(move |e| e.kind == kind)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn entry(kind: ContextKind, reference: &str) -> ContextEntry {
94        ContextEntry {
95            kind,
96            source: "person".to_owned(),
97            reference: reference.to_owned(),
98            sha256: "00".to_owned(),
99            bytes: 2,
100            cut: false,
101        }
102    }
103
104    #[test]
105    fn the_digest_does_not_depend_on_the_order_parts_were_added() {
106        let h = |b: &[u8]| format!("{}", b.len());
107        let a = ContextManifest::of(
108            vec![
109                entry(ContextKind::Issue, "#42"),
110                entry(ContextKind::Story, "/triad"),
111            ],
112            h,
113        );
114        let b = ContextManifest::of(
115            vec![
116                entry(ContextKind::Story, "/triad"),
117                entry(ContextKind::Issue, "#42"),
118            ],
119            h,
120        );
121        assert_eq!(a, b);
122        assert_eq!(a.entries[0].kind, ContextKind::Story);
123        assert_eq!(a.of_kind(ContextKind::Issue).count(), 1);
124    }
125}