返回 CodeWhale
survival_contract.rs
根目录 / crates / tui / src / compaction / survival_contract.rs
1 //! Schema table for the compaction survival contract.
2 //!
3 //! Compiled only under `cfg(test)`: this is compile-time documentation of
4 //! the field table, not the runtime enforcement path (`last_round.rs`).
5 //! Field names and locations are language-invariant protocol data. The B1
6 //! strategy (Rust compaction path) enforces them over the session-tree
7 //! journal / API transcript. See [`SURVIVAL_CONTRACT.md`](./SURVIVAL_CONTRACT.md).
8
9 /// Documented schema version. Bump when a survive/summarize/prune rule
10 /// changes; TS/Go strategies pin this number.
11 pub const SURVIVAL_CONTRACT_VERSION: u32 = 2;
12
13 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14 pub enum SurvivalRule {
15 Always,
16 LastRoundVerbatim,
17 LastRoundBounded,
18 LatestOnly,
19 RestateFromLiveState,
20 Summarize,
21 Prune,
22 NeverDurable,
23 }
24
25 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26 pub struct SurvivalField {
27 pub name: &'static str,
28 pub location: &'static str,
29 pub rule: SurvivalRule,
30 }
31
32 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33 pub struct EntrySurvival {
34 pub kind: &'static str,
35 pub fields: &'static [SurvivalField],
36 }
37
38 /// Protocol journal kinds plus session-tree `message` / `system` carriers.
39 pub const ENTRY_SURVIVAL: &[EntrySurvival] = &[
40 EntrySurvival {
41 kind: "header",
42 fields: &[
43 SurvivalField {
44 name: "id",
45 location: "JournalEntry.id",
46 rule: SurvivalRule::Always,
47 },
48 SurvivalField {
49 name: "created_at",
50 location: "JournalEntry.created_at",
51 rule: SurvivalRule::Always,
52 },
53 SurvivalField {
54 name: "secrets",
55 location: "payload",
56 rule: SurvivalRule::NeverDurable,
57 },
58 ],
59 },
60 EntrySurvival {
61 kind: "user",
62 fields: &[
63 SurvivalField {
64 name: "text",
65 location: "payload / Message.content[type=text].text",
66 rule: SurvivalRule::LastRoundVerbatim,
67 },
68 SurvivalField {
69 name: "older_turns",
70 location: "payload / Message.content[type=text].text",
71 rule: SurvivalRule::Summarize,
72 },
73 ],
74 },
75 EntrySurvival {
76 kind: "assistant",
77 fields: &[
78 SurvivalField {
79 name: "text",
80 location: "payload / Message.content[type=text].text",
81 rule: SurvivalRule::LastRoundVerbatim,
82 },
83 SurvivalField {
84 name: "tool_use.id",
85 location: "Message.content[type=tool_use].id",
86 rule: SurvivalRule::LastRoundVerbatim,
87 },
88 ],
89 },
90 EntrySurvival {
91 kind: "tool_result",
92 fields: &[
93 SurvivalField {
94 name: "tool_use_id",
95 location: "Message.content[type=tool_result].tool_use_id",
96 rule: SurvivalRule::LastRoundVerbatim,
97 },
98 SurvivalField {
99 name: "content",
100 location: "Message.content[type=tool_result].content",
101 rule: SurvivalRule::LastRoundBounded,
102 },
103 SurvivalField {
104 name: "is_error",
105 location: "Message.content[type=tool_result].is_error",
106 rule: SurvivalRule::LastRoundVerbatim,
107 },
108 SurvivalField {
109 name: "older_results",
110 location: "Message.content[type=tool_result].content",
111 rule: SurvivalRule::Prune,
112 },
113 ],
114 },
115 EntrySurvival {
116 kind: "compaction",
117 fields: &[
118 SurvivalField {
119 name: "summary",
120 location: "payload.summary / checkpoint user text",
121 rule: SurvivalRule::LatestOnly,
122 },
123 SurvivalField {
124 name: "coverage",
125 location: "CompactionCoverage / /context inspector",
126 rule: SurvivalRule::LatestOnly,
127 },
128 ],
129 },
130 EntrySurvival {
131 kind: "branch_summary",
132 fields: &[
133 SurvivalField {
134 name: "branch_id",
135 location: "payload.branch_id",
136 rule: SurvivalRule::Always,
137 },
138 SurvivalField {
139 name: "summary",
140 location: "payload.summary",
141 rule: SurvivalRule::Always,
142 },
143 ],
144 },
145 EntrySurvival {
146 kind: "message",
147 fields: &[
148 SurvivalField {
149 name: "role",
150 location: "SessionEntryKind::Message.message.role",
151 rule: SurvivalRule::LastRoundVerbatim,
152 },
153 SurvivalField {
154 name: "content",
155 location: "SessionEntryKind::Message.message.content",
156 rule: SurvivalRule::LastRoundBounded,
157 },
158 ],
159 },
160 EntrySurvival {
161 kind: "system",
162 fields: &[SurvivalField {
163 name: "content",
164 location: "SessionEntryKind::System.content",
165 rule: SurvivalRule::Always,
166 }],
167 },
168 ];
169
170 pub const LIVE_STATE_FIELDS: &[SurvivalField] = &[
171 SurvivalField {
172 name: "anchors",
173 location: ".codewhale/anchors.md restated on the checkpoint",
174 rule: SurvivalRule::RestateFromLiveState,
175 },
176 SurvivalField {
177 name: "branch_worktree",
178 location: "live git / workspace",
179 rule: SurvivalRule::RestateFromLiveState,
180 },
181 SurvivalField {
182 name: "background_work_handles",
183 location: "session + worker registry",
184 rule: SurvivalRule::RestateFromLiveState,
185 },
186 SurvivalField {
187 name: "billed_parent_prompt_tokens",
188 location: "TurnContext.latest_parent_input_tokens",
189 rule: SurvivalRule::LatestOnly,
190 },
191 SurvivalField {
192 name: "compaction_receipt",
193 location: "checkpoint user message + HistoryCell::System",
194 rule: SurvivalRule::LatestOnly,
195 },
196 ];
197
198 #[cfg(test)]
199 mod tests {
200 use super::*;
201
202 const SCHEMA_DOC: &str = include_str!("SURVIVAL_CONTRACT.md");
203
204 #[test]
205 fn schema_doc_lists_every_entry_kind_and_survive_field() {
206 let kinds: Vec<_> = ENTRY_SURVIVAL.iter().map(|entry| entry.kind).collect();
207 assert!(kinds.contains(&"user"));
208 assert!(kinds.contains(&"tool_result"));
209 assert!(kinds.contains(&"compaction"));
210 for entry in ENTRY_SURVIVAL {
211 assert!(
212 SCHEMA_DOC.contains(&format!("`{}`", entry.kind)),
213 "SURVIVAL_CONTRACT.md must document kind {}",
214 entry.kind
215 );
216 for field in entry.fields.iter().filter(|field| {
217 matches!(
218 field.rule,
219 SurvivalRule::Always
220 | SurvivalRule::LastRoundVerbatim
221 | SurvivalRule::LastRoundBounded
222 | SurvivalRule::LatestOnly
223 )
224 }) {
225 let token = field.name.split('.').next().unwrap_or(field.name);
226 assert!(
227 SCHEMA_DOC.contains(token) || SCHEMA_DOC.contains(field.name),
228 "SURVIVAL_CONTRACT.md must locate {}.{}",
229 entry.kind,
230 field.name
231 );
232 }
233 }
234 for field in LIVE_STATE_FIELDS {
235 assert!(
236 SCHEMA_DOC.to_ascii_lowercase().contains("anchor")
237 && SCHEMA_DOC.contains("receipt"),
238 "live-state field {} must stay in the schema doc",
239 field.name
240 );
241 }
242 assert!(SCHEMA_DOC.contains("Failed compact must not replace live history"));
243 assert!(
244 SCHEMA_DOC.contains(&format!("Schema version {SURVIVAL_CONTRACT_VERSION}")),
245 "SURVIVAL_CONTRACT.md must pin schema version {SURVIVAL_CONTRACT_VERSION}"
246 );
247 }
248
249 #[test]
250 fn last_round_kinds_have_verbatim_or_bounded_rules() {
251 for kind in ["user", "assistant", "tool_result", "message"] {
252 let entry = ENTRY_SURVIVAL
253 .iter()
254 .find(|entry| entry.kind == kind)
255 .unwrap_or_else(|| panic!("missing {kind}"));
256 assert!(
257 entry.fields.iter().any(|field| matches!(
258 field.rule,
259 SurvivalRule::LastRoundVerbatim | SurvivalRule::LastRoundBounded
260 )),
261 "{kind} must declare a last-round survival rule"
262 );
263 }
264 }
265
266 #[test]
267 fn compaction_kind_keeps_latest_receipt_only() {
268 let entry = ENTRY_SURVIVAL
269 .iter()
270 .find(|entry| entry.kind == "compaction")
271 .expect("compaction kind");
272 assert!(
273 entry
274 .fields
275 .iter()
276 .any(|field| field.name == "summary" && field.rule == SurvivalRule::LatestOnly)
277 );
278 assert!(
279 LIVE_STATE_FIELDS
280 .iter()
281 .any(|field| field.name == "compaction_receipt")
282 );
283 assert!(
284 LIVE_STATE_FIELDS
285 .iter()
286 .any(|field| field.name == "anchors")
287 );
288 }
289 }
290
290 lines RUST