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