1use std::collections::BTreeMap;
44use std::path::Path;
45
46use serde::{Deserialize, Serialize};
47
48use crate::hook::END_START_BUDGET;
49
50pub const STEP: f64 = 1.5;
53
54pub const HOLD: f64 = 0.85;
57
58pub const REFERENCE_LESSONS: u64 = 30;
63
64pub const MIN_CALLS: u64 = 5;
69
70pub const EXPLORE_SHARE: f64 = 0.2;
75
76pub const FAILED_EXPLORE_SHARE: f64 = EXPLORE_SHARE / 4.0;
81
82const Z95: f64 = 1.96;
84
85pub const CACHE: &str = "judge-length.json";
87
88pub const VERSION: u32 = 2;
92
93#[must_use]
95pub fn edge(j: i32) -> usize {
96 (END_START_BUDGET as f64 * STEP.powi(j)).round() as usize
97}
98
99#[must_use]
101pub fn step_of(chars: usize) -> i32 {
102 let mut j = 0;
103 while chars > edge(j) {
104 j += 1;
105 }
106 while chars <= edge(j - 1) && edge(j - 1) > 0 {
107 j -= 1;
108 }
109 j
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub struct Call {
115 pub model: String,
117 pub chars: usize,
119 pub sent: u64,
121 pub answered: u64,
122}
123
124#[must_use]
127pub fn parse(line: &str) -> Option<Call> {
128 let (rest, failed) = match line.strip_prefix("judge ") {
129 Some(rest) => (rest, false),
130 None => (line.strip_prefix("judge-failed ")?, true),
131 };
132 let field = |k: &str| rest.split_whitespace().find_map(|w| w.strip_prefix(k));
133 let num = |k: &str| field(k).and_then(|v| v.parse::<u64>().ok());
134 let sent = num("pending=").filter(|n| *n > 0)?;
135 if field("at=") != Some(crate::outcome::AT_END) {
136 return None;
137 }
138 let chars = num("chars=")? as usize;
139 let answered = match field("verdicts=") {
140 _ if failed => 0,
141 Some(v) => v
142 .split(',')
143 .filter_map(|p| p.split_once(':'))
144 .filter(|(word, _)| *word != crate::predict::Verdict::Unclear.word())
145 .filter_map(|(_, n)| n.parse::<u64>().ok())
146 .sum(),
147 None => num("resolved=").unwrap_or(0),
148 };
149 Some(Call { model: field("model=").unwrap_or_default().to_string(), chars, sent, answered: answered.min(sent) })
150}
151
152#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155pub struct Tally {
156 pub sent: u64,
157 pub answered: u64,
158 pub calls: u64,
159 pub sent_sq: u64,
160 pub answered_sq: u64,
161 pub cross: u64,
162}
163
164impl Tally {
165 pub fn add(&mut self, sent: u64, answered: u64) {
167 self.sent += sent;
168 self.answered += answered;
169 self.calls += 1;
170 self.sent_sq += sent * sent;
171 self.answered_sq += answered * answered;
172 self.cross += sent * answered;
173 }
174
175 pub fn merge(&mut self, o: &Tally) {
177 self.sent += o.sent;
178 self.answered += o.answered;
179 self.calls += o.calls;
180 self.sent_sq += o.sent_sq;
181 self.answered_sq += o.answered_sq;
182 self.cross += o.cross;
183 }
184
185 #[must_use]
187 pub fn share(&self) -> f64 {
188 if self.sent == 0 { 0.0 } else { self.answered as f64 / self.sent as f64 }
189 }
190
191 #[must_use]
196 pub fn effective(&self) -> f64 {
197 let n = self.sent as f64;
198 let p = self.share();
199 if self.calls < 2 || p <= 0.0 || p >= 1.0 {
200 return n;
201 }
202 let spread = (self.answered_sq as f64 - 2.0 * p * self.cross as f64 + p * p * self.sent_sq as f64).max(0.0);
204 let c = self.calls as f64;
205 let var = c / (c - 1.0) * spread / (n * n);
206 if var <= 0.0 { n } else { (p * (1.0 - p) / var).min(n) }
207 }
208
209 #[must_use]
211 pub fn interval(&self) -> (f64, f64) {
212 wilson_of(self.share(), self.effective())
213 }
214}
215
216pub type Steps = BTreeMap<i32, Tally>;
218
219#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
222struct Cache {
223 version: u32,
224 stamp: String,
225 models: BTreeMap<String, Vec<(i32, Tally)>>,
226}
227
228fn stamp(state: &Path) -> String {
231 let (mut files, mut bytes, mut newest) = (0u64, 0u64, 0u128);
232 for e in std::fs::read_dir(state).into_iter().flatten().flatten() {
233 if e.path().extension().is_some_and(|x| x == "cost") {
234 let Ok(m) = e.metadata() else { continue };
235 files += 1;
236 bytes += m.len();
237 let t = m.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_nanos());
238 newest = newest.max(t);
239 }
240 }
241 format!("{files}:{bytes}:{newest}")
242}
243
244fn count(state: &Path) -> BTreeMap<String, Steps> {
246 let mut out: BTreeMap<String, Steps> = BTreeMap::new();
247 for e in std::fs::read_dir(state).into_iter().flatten().flatten() {
248 if e.path().extension().is_none_or(|x| x != "cost") {
249 continue;
250 }
251 for c in std::fs::read_to_string(e.path()).unwrap_or_default().lines().filter_map(parse) {
252 out.entry(c.model).or_default().entry(step_of(c.chars)).or_default().add(c.sent, c.answered);
253 }
254 }
255 out
256}
257
258#[must_use]
262pub fn counts(state: &Path) -> BTreeMap<String, Steps> {
263 let path = state.join(CACHE);
264 let now = stamp(state);
265 if let Some(c) = std::fs::read_to_string(&path).ok().and_then(|t| serde_json::from_str::<Cache>(&t).ok())
266 && c.version == VERSION
267 && c.stamp == now
268 {
269 return c.models.into_iter().map(|(m, v)| (m, v.into_iter().collect())).collect();
270 }
271 let counted = count(state);
272 let cache = Cache { version: VERSION, stamp: now, models: counted.iter().map(|(m, s)| (m.clone(), s.iter().map(|(j, t)| (*j, *t)).collect())).collect() };
273 if let Ok(text) = serde_json::to_string(&cache) {
274 static WRITER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
276 let tmp = state.join(format!("{CACHE}.{}.{}", std::process::id(), WRITER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)));
277 if std::fs::write(&tmp, text).is_ok() && std::fs::rename(&tmp, &path).is_err() {
278 let _ = std::fs::remove_file(&tmp);
279 }
280 }
281 counted
282}
283
284#[must_use]
286pub fn wilson(k: u64, n: u64) -> (f64, f64) {
287 wilson_of(if n == 0 { 0.0 } else { k as f64 / n as f64 }, n as f64)
288}
289
290#[must_use]
293pub fn wilson_of(p: f64, n: f64) -> (f64, f64) {
294 if n <= 0.0 {
295 return (0.0, 1.0);
296 }
297 let z2 = Z95 * Z95;
298 let centre = (p + z2 / (2.0 * n)) / (1.0 + z2 / n);
299 let half = Z95 * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt() / (1.0 + z2 / n);
300 ((centre - half).max(0.0), (centre + half).min(1.0))
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
305#[serde(rename_all = "snake_case")]
306pub enum Decision {
307 Reference,
310 Below,
312 Holds,
314 Fails,
316 Open,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize)]
323pub struct Step {
324 pub step: i32,
325 pub upto: usize,
327 pub sent: u64,
328 pub answered: u64,
329 pub calls: u64,
331 pub lo: f64,
333 pub hi: f64,
334 pub decision: Decision,
335}
336
337#[derive(Debug, Clone, PartialEq, Serialize)]
339pub struct Length {
340 pub model: String,
342 pub chars: usize,
344 pub start: bool,
346 pub line: Option<f64>,
348 pub steps: Vec<Step>,
350 pub explore: Option<usize>,
352 pub psi: f64,
355}
356
357impl Length {
358 #[must_use]
360 pub fn of(model: &str, steps: &Steps) -> Length {
361 let mut pooled = Tally::default();
363 let reference = steps.iter().find_map(|(j, t)| {
364 pooled.merge(t);
365 (pooled.sent >= REFERENCE_LESSONS && pooled.calls >= MIN_CALLS).then_some((*j, pooled.share()))
366 });
367 let line = reference.map(|(_, share)| HOLD * share);
368 let decide = |j: i32| -> Decision {
369 let Some((r, _)) = reference else { return Decision::Open };
370 match j.cmp(&r) {
371 std::cmp::Ordering::Less => return Decision::Below,
372 std::cmp::Ordering::Equal => return Decision::Reference,
373 std::cmp::Ordering::Greater => {}
374 }
375 let t = steps.get(&j).copied().unwrap_or_default();
376 if t.calls < MIN_CALLS {
377 return Decision::Open;
378 }
379 let (lo, hi) = t.interval();
380 let l = line.unwrap_or(0.0);
381 if lo >= l {
382 Decision::Holds
383 } else if hi < l {
384 Decision::Fails
385 } else {
386 Decision::Open
387 }
388 };
389 let holds = |j: i32| matches!(decide(j), Decision::Holds | Decision::Reference | Decision::Below);
392 let mut j = 0;
393 while decide(j) == Decision::Fails {
394 j -= 1;
395 }
396 if j == 0 {
397 while holds(j + 1) {
398 j += 1;
399 }
400 }
401 let (explore, psi) = match decide(j + 1) {
402 Decision::Open => (Some(edge(j + 1)), EXPLORE_SHARE),
403 Decision::Fails => (Some(edge(j + 1)), FAILED_EXPLORE_SHARE),
404 _ => (None, 0.0),
405 };
406 let mut shown: Vec<Step> = steps
407 .iter()
408 .map(|(&k, t)| {
409 let (lo, hi) = t.interval();
410 Step { step: k, upto: edge(k), sent: t.sent, answered: t.answered, calls: t.calls, lo, hi, decision: decide(k) }
411 })
412 .collect();
413 if explore.is_some() && !steps.contains_key(&(j + 1)) {
414 shown.push(Step { step: j + 1, upto: edge(j + 1), sent: 0, answered: 0, calls: 0, lo: 0.0, hi: 1.0, decision: Decision::Open });
415 }
416 Length { model: model.to_string(), chars: edge(j), start: j == 0, line, steps: shown, explore, psi }
417 }
418}
419
420fn of_model(all: &BTreeMap<String, Steps>, model: &str, unnamed: &str) -> Steps {
423 let mut out = all.get(model).cloned().unwrap_or_default();
424 if !unnamed.is_empty() && model == unnamed {
425 for (j, t) in all.get("").into_iter().flatten() {
426 out.entry(*j).or_default().merge(t);
427 }
428 }
429 out
430}
431
432#[must_use]
434pub fn length(state: &Path, model: &str) -> Length {
435 let all = counts(state);
436 let unnamed = if all.contains_key("") { crate::hook::judge_model("") } else { String::new() };
438 Length::of(model, &of_model(&all, model, &unnamed))
439}
440
441#[must_use]
445pub fn lengths(state: &Path, current: &str) -> Vec<Length> {
446 let all = counts(state);
447 let unnamed = crate::hook::judge_model("");
448 let mut models: std::collections::BTreeSet<String> = all.keys().filter(|m| !m.is_empty()).cloned().collect();
449 if all.contains_key("") && !unnamed.is_empty() {
450 models.insert(unnamed.clone());
451 }
452 if !current.is_empty() {
453 models.insert(current.to_string());
454 }
455 models.into_iter().map(|m| Length::of(&m, &of_model(&all, &m, &unnamed))).collect()
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 fn steps(rows: &[(usize, u64, u64)]) -> Steps {
465 rows.iter()
466 .map(|&(upto, n, a)| {
467 let c = n.min(10);
468 let mut t = Tally::default();
469 for i in 0..c {
470 t.add(n / c + u64::from(i < n % c), a / c + u64::from(i < a % c));
471 }
472 (step_of(upto), t)
473 })
474 .collect()
475 }
476
477 fn calls(rows: &[(u64, u64)]) -> Tally {
479 let mut t = Tally::default();
480 for &(n, a) in rows {
481 t.add(n, a);
482 }
483 t
484 }
485
486 #[test]
487 fn steps_are_half_again_apart_around_the_start_budget() {
488 assert_eq!((edge(-2), edge(-1), edge(0), edge(1), edge(2)), (32_000, 48_000, 72_000, 108_000, 162_000));
489 assert_eq!((step_of(72_000), step_of(72_001), step_of(48_001), step_of(48_000), step_of(1)), (0, 1, 0, -1, step_of(1)));
490 assert!(step_of(1) < -10 && edge(step_of(1)) >= 1);
491 for c in [1, 999, 31_999, 32_000, 100_000, 2_000_000] {
492 let j = step_of(c);
493 assert!(c <= edge(j) && c > edge(j - 1), "{c}: {j}");
494 }
495 }
496
497 #[test]
499 fn the_wilson_interval_matches_known_values() {
500 let (lo, hi) = wilson(50, 100);
501 assert!((lo - 0.4038).abs() < 1e-3 && (hi - 0.5962).abs() < 1e-3, "{lo} {hi}");
502 let (lo, hi) = wilson(0, 10);
503 assert!(lo == 0.0 && (hi - 0.2775).abs() < 1e-3, "{hi}");
504 assert_eq!(wilson(0, 0), (0.0, 1.0));
505 }
506
507 #[test]
511 fn every_cost_line_format_gives_what_it_holds() {
512 let check = "judge call=3 pending=4 resolved=2 chars=900 returned=a,b,c unmatched= verdicts=changed:1,ignored:1,not_arisen:0,consequence_remains:1,unclear:1 reason_chars=40 model=claude:sonnet prompt=30000";
513 assert_eq!(parse(check), None, "a check's line: another task");
514 let end = "judge call=5 pending=10 resolved=6 chars=70000 returned= unmatched= verdicts=changed:6,ignored:0,not_arisen:0,consequence_remains:0,unclear:4 reason_chars=9 model=codex:gpt-5 at=end batch=1/3 budget=72000 cut=after";
515 assert_eq!(parse(end), Some(Call { model: "codex:gpt-5".into(), chars: 70_000, sent: 10, answered: 6 }));
516 let end_075 = "judge call=2 pending=7 resolved=1 chars=40000 returned= unmatched= verdicts=changed:1,ignored:0,not_arisen:0,consequence_remains:2,unclear:4 reason_chars=9 at=end";
517 assert_eq!(parse(end_075), Some(Call { model: String::new(), chars: 40_000, sent: 7, answered: 3 }));
518 let failed = "judge-failed pending=12 chars=71000 model=claude:sonnet at=end batch=2/3 budget=72000 cut=after";
519 assert_eq!(parse(failed), Some(Call { model: "claude:sonnet".into(), chars: 71_000, sent: 12, answered: 0 }));
520 assert_eq!(parse("judge call=2 pending=3 resolved=2 chars=4100"), None, "a check's line before 0.7.6");
521 assert_eq!(parse("judge call=2 pending=3 resolved=2 chars=4100 at=end").map(|c| c.answered), Some(2), "before 0.7.5: resolved=");
522 assert_eq!(parse("judge call=2 pending=0 resolved=0 chars=4100 at=end"), None);
523 assert_eq!(parse("block questions=2 chars=600"), None);
524 }
525
526 #[test]
530 fn one_call_never_decides_a_step_and_disagreeing_calls_count_for_less() {
531 let mut base = steps(&[(4_000, 59, 15)]);
532 base.insert(step_of(100_000), calls(&[(2, 2)]));
534 let up = Length::of("m", &base);
535 assert_eq!((up.chars, up.start, up.explore), (72_000, true, Some(108_000)), "{up:?}");
536 let mut base = steps(&[(4_000, 59, 15)]);
538 base.insert(step_of(70_000), calls(&[(30, 0)]));
539 let down = Length::of("m", &base);
540 assert_eq!((down.chars, down.start, down.psi), (72_000, true, EXPLORE_SHARE), "{down:?}");
541 assert_eq!(down.steps.iter().find(|s| s.upto == 72_000).map(|s| s.decision), Some(Decision::Open));
542 let reference = (32_000, 400, 200);
545 let clustered = calls(&[(30, 30), (30, 0), (30, 0), (30, 24), (30, 0), (30, 0)]);
546 assert!(wilson(clustered.answered, clustered.sent).1 < HOLD * 0.5, "independent: would fail");
547 assert!(clustered.effective() < 20.0, "{}", clustered.effective());
548 let mut st = steps(&[reference, (72_000, 300, 150)]);
549 st.insert(step_of(100_000), clustered);
550 let l = Length::of("m", &st);
551 assert_eq!(l.steps.iter().find(|s| s.upto == 108_000).map(|s| s.decision), Some(Decision::Open), "{l:?}");
552 let mut st = steps(&[reference, (72_000, 300, 150)]);
554 st.insert(step_of(100_000), calls(&[(30, 9); 6]));
555 assert_eq!(Length::of("m", &st).steps.iter().find(|s| s.upto == 108_000).map(|s| s.decision), Some(Decision::Fails));
556 }
557
558 #[test]
560 fn the_reference_pools_the_smallest_steps() {
561 let l = Length::of("m", &steps(&[(10_000, 20, 10), (21_333, 20, 10), (72_000, 300, 150)]));
562 assert_eq!(l.steps.iter().filter(|s| s.sent > 0).map(|s| s.decision).collect::<Vec<_>>(), [Decision::Below, Decision::Reference, Decision::Holds]);
563 assert!((l.line.unwrap() - HOLD * 0.5).abs() < 1e-9);
564 let mut few = Steps::new();
566 few.insert(step_of(10_000), calls(&[(40, 20), (40, 20)]));
567 assert_eq!(Length::of("m", &few).line, None);
568 }
569
570 #[test]
572 fn thin_evidence_keeps_the_start_and_explores_the_step_above() {
573 let none = Length::of("m", &Steps::new());
574 assert_eq!((none.chars, none.start, none.explore, none.psi, none.line), (72_000, true, Some(108_000), EXPLORE_SHARE, None));
575 assert_eq!(none.steps.len(), 1, "the explored step shows: {:?}", none.steps);
576 let thin = Length::of("m", &steps(&[(32_000, 40, 20), (108_000, 5, 2)]));
578 assert_eq!((thin.chars, thin.start, thin.explore), (72_000, true, Some(108_000)));
579 assert_eq!(thin.steps.iter().map(|s| s.decision).collect::<Vec<_>>(), [Decision::Reference, Decision::Open]);
580 let small = Length::of("m", &steps(&[(32_000, 12, 12), (108_000, 10, 1)]));
582 assert_eq!((small.chars, small.line, small.explore), (72_000, None, Some(108_000)));
583 }
584
585 #[test]
588 fn flat_logs_step_the_budget_up() {
589 let flat = Length::of("m", &steps(&[(32_000, 400, 200), (72_000, 300, 150), (108_000, 300, 151), (162_000, 300, 149), (243_000, 3, 1)]));
590 assert_eq!((flat.chars, flat.start, flat.explore, flat.psi), (162_000, false, Some(243_000), EXPLORE_SHARE));
591 let d: Vec<Decision> = flat.steps.iter().map(|s| s.decision).collect();
592 assert_eq!(d, [Decision::Reference, Decision::Holds, Decision::Holds, Decision::Holds, Decision::Open]);
593 let capped = Length::of("m", &steps(&[(32_000, 400, 200), (108_000, 300, 150), (162_000, 400, 100)]));
595 assert_eq!((capped.chars, capped.explore, capped.psi), (108_000, Some(162_000), FAILED_EXPLORE_SHARE));
596 }
597
598 #[test]
601 fn degrading_logs_step_the_budget_down() {
602 let worse = Length::of("m", &steps(&[(21_333, 400, 300), (32_000, 300, 250), (48_000, 300, 230), (72_000, 300, 120)]));
603 assert_eq!((worse.chars, worse.start), (48_000, false), "{worse:?}");
604 assert_eq!((worse.explore, worse.psi), (Some(72_000), FAILED_EXPLORE_SHARE), "72k failed: explored at the floor, so it can hold again");
605 let d: Vec<Decision> = worse.steps.iter().map(|s| s.decision).collect();
606 assert_eq!(d, [Decision::Reference, Decision::Holds, Decision::Holds, Decision::Fails]);
607 let open = Length::of("m", &steps(&[(21_333, 400, 300), (32_000, 300, 250), (48_000, 300, 180), (72_000, 300, 120)]));
609 assert_eq!((open.chars, open.explore, open.psi), (48_000, Some(72_000), FAILED_EXPLORE_SHARE), "{open:?}");
610 assert_eq!(open.steps[2].decision, Decision::Open);
611 let deep = Length::of("m", &steps(&[(21_333, 400, 300), (32_000, 300, 250), (48_000, 300, 120), (72_000, 300, 120)]));
612 assert_eq!(deep.chars, 32_000);
613 let bottom = Length::of("m", &steps(&[(48_000, 300, 270), (72_000, 300, 30)]));
615 assert_eq!(bottom.chars, 48_000);
616 }
617
618 #[test]
621 fn the_counts_are_cached_until_the_cost_lines_change() {
622 let d = std::env::temp_dir().join(format!("borg-judge-length-{}", std::process::id()));
623 let _ = std::fs::remove_dir_all(&d);
624 std::fs::create_dir_all(&d).unwrap();
625 let end = |n: u64, a: u64, model: &str| {
626 format!(
627 "judge call=1 pending={n} resolved={a} chars=60000 returned= unmatched= verdicts=changed:{a},ignored:0,not_arisen:0,consequence_remains:0,unclear:{} reason_chars=1{model} at=end\n",
628 n - a
629 )
630 };
631 std::fs::write(d.join("a.cost"), end(10, 4, " model=x:y") + &end(6, 6, "") + "block questions=1 chars=3\n").unwrap();
632 let c = counts(&d);
633 assert_eq!((c["x:y"][&0].sent, c["x:y"][&0].answered, c["x:y"][&0].calls), (10, 4, 1));
634 assert_eq!((c[""][&0].sent, c[""][&0].answered), (6, 6));
635 let cached: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(d.join(CACHE)).unwrap()).unwrap();
636 assert_eq!(cached["version"], VERSION);
637 let all = counts(&d);
639 let pair = |t: &Tally| (t.sent, t.answered, t.calls);
640 assert_eq!(pair(&of_model(&all, "x:y", "x:y")[&0]), (16, 10, 2));
641 assert_eq!(pair(&of_model(&all, "x:y", "z:w")[&0]), (10, 4, 1));
642 std::fs::write(d.join("b.cost"), end(5, 5, " model=x:y")).unwrap();
644 assert_eq!(pair(&counts(&d)["x:y"][&0]), (15, 9, 2));
645 let mut stale: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(d.join(CACHE)).unwrap()).unwrap();
647 stale["version"] = serde_json::json!(VERSION + 1);
648 stale["models"]["x:y"][0][1]["sent"] = serde_json::json!(1);
649 std::fs::write(d.join(CACHE), stale.to_string()).unwrap();
650 assert_eq!(pair(&counts(&d)["x:y"][&0]), (15, 9, 2));
651 std::fs::write(d.join("c.cost"), end(5, 1, " model=x:y")).unwrap();
653 std::thread::scope(|s| {
654 for _ in 0..8 {
655 s.spawn(|| assert_eq!(pair(&counts(&d)["x:y"][&0]), (20, 10, 3)));
656 }
657 });
658 let cache: Cache = serde_json::from_str(&std::fs::read_to_string(d.join(CACHE)).unwrap()).unwrap();
659 assert_eq!(cache.version, VERSION);
660 assert!(std::fs::read_dir(&d).unwrap().all(|e| !e.unwrap().file_name().to_string_lossy().starts_with(&format!("{CACHE}."))), "no temp file left");
661 assert!(lengths(&d, "q:r").iter().any(|l| l.model == "q:r" && l.start));
663 let _ = std::fs::remove_dir_all(&d);
664 }
665}