1use serde_json::{Map, Value, json};
9
10use crate::anchor::{Anchor, AnchorMode, AnchorRole};
11use crate::model::{Page, PageStatus};
12use crate::replica::{PageView, SectionView};
13use crate::sections;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct EventSpec {
18 pub op: &'static str,
20 pub target: String,
21 pub node_type: Option<&'static str>,
23 pub body: Map<String, Value>,
25 pub base_event: Option<String>,
27}
28
29pub type NewId<'a> = &'a mut dyn FnMut(&str) -> String;
31
32fn obj(v: Value) -> Map<String, Value> {
33 match v {
34 Value::Object(m) => m,
35 _ => Map::new(),
36 }
37}
38
39pub fn anchor_value(a: &Anchor) -> Value {
41 let mut m = obj(json!({ "ref": a.reference(), "digest": a.digest }));
42 if let Some(c) = &a.commit {
43 m.insert("commit".into(), Value::from(c.as_str()));
44 }
45 if a.mode == AnchorMode::Shape {
46 m.insert("mode".into(), Value::from("shape"));
47 }
48 if a.role == AnchorRole::Cites {
49 m.insert("role".into(), Value::from("cites"));
50 }
51 Value::Object(m)
52}
53
54fn status_str(s: PageStatus) -> &'static str {
55 match s {
56 PageStatus::Draft => "draft",
57 PageStatus::Canonical => "canonical",
58 PageStatus::Retired => "retired",
59 }
60}
61
62fn node_add(node_type: &'static str, target: String, body: Value) -> EventSpec {
63 EventSpec {
64 op: "node.add",
65 target,
66 node_type: Some(node_type),
67 body: obj(body),
68 base_event: None,
69 }
70}
71
72fn edge_add(kind: &str, from: &str, to: &str, new_id: NewId<'_>) -> EventSpec {
73 EventSpec {
74 op: "edge.add",
75 target: new_id("e"),
76 node_type: None,
77 body: obj(json!({ "kind": kind, "from": from, "to": to })),
78 base_event: None,
79 }
80}
81
82pub fn create_page(
87 page: &Page,
88 links_to: &[String],
89 new_id: NewId<'_>,
90) -> (Vec<EventSpec>, String) {
91 let id = new_id("n");
92 let mut body = obj(json!({
93 "slug": page.slug,
94 "title": page.title,
95 "type": page.kind,
96 "status": status_str(page.status),
97 }));
98 if !page.anchors.is_empty() {
99 body.insert(
100 "anchors".into(),
101 page.anchors.iter().map(anchor_value).collect(),
102 );
103 }
104 let mut out = vec![node_add("page", id.clone(), Value::Object(body))];
105 for s in &page.sections {
106 let sid = new_id("n");
107 out.push(node_add(
108 "section",
109 sid.clone(),
110 json!({ "body": s.text, "order": s.order }),
111 ));
112 out.push(edge_add("part_of", &sid, &id, new_id));
113 }
114 for n in &page.notes {
115 let nid = new_id("n");
116 let covers: Vec<Value> = n
117 .covers
118 .iter()
119 .map(|r| json!({ "ref": r, "digest": Value::Null }))
120 .collect();
121 let mut nb = obj(json!({ "body": n.body, "status": "open" }));
122 if !covers.is_empty() {
123 nb.insert("covers".into(), Value::Array(covers));
124 }
125 out.push(node_add("note", nid.clone(), Value::Object(nb)));
126 out.push(edge_add("about", &nid, &id, new_id));
127 }
128 for to in links_to {
129 out.push(edge_add("links_to", &id, to, new_id));
130 }
131 (out, id)
132}
133
134pub fn field_set(target: &str, field: &str, value: Value, base: &str) -> EventSpec {
136 let mut body = Map::new();
137 body.insert("field".into(), Value::from(field));
138 body.insert("value".into(), value);
139 EventSpec {
140 op: "field.set",
141 target: target.to_owned(),
142 node_type: None,
143 body,
144 base_event: Some(base.to_owned()),
145 }
146}
147
148pub fn edit_section(section: &SectionView, body: &str) -> EventSpec {
150 field_set(§ion.id, "body", Value::from(body), §ion.head)
151}
152
153pub fn insert_section(
157 page: &PageView,
158 after: Option<usize>,
159 text: &str,
160 new_id: NewId<'_>,
161) -> Vec<EventSpec> {
162 let secs = &page.sections;
163 let before = after.and_then(|i| secs.get(i)).map(|s| s.order);
164 let next = after.map_or(0, |i| i + 1);
165 let behind = secs.get(next).map(|s| s.order);
166 let mut out = Vec::new();
167 let order = sections::order_between(before, behind).unwrap_or_else(|| {
168 let fresh = sections::renumber(secs.len() + 1);
171 for (i, s) in secs.iter().enumerate() {
172 let new = fresh[if i >= next { i + 1 } else { i }];
173 if new != s.order {
174 out.push(field_set(&s.id, "order", Value::from(new), &s.head));
175 }
176 }
177 fresh[next]
178 });
179 let sid = new_id("n");
180 out.push(node_add(
181 "section",
182 sid.clone(),
183 json!({ "body": text, "order": order }),
184 ));
185 out.push(edge_add("part_of", &sid, &page.page.id, new_id));
186 out
187}
188
189pub fn add_note(
192 about: &str,
193 body: &str,
194 covers: &[(String, Option<String>)],
195 new_id: NewId<'_>,
196) -> Vec<EventSpec> {
197 let id = new_id("n");
198 let mut nb = obj(json!({ "body": body, "status": "open" }));
199 if !covers.is_empty() {
200 let list: Vec<Value> = covers
201 .iter()
202 .map(|(r, d)| json!({ "ref": r, "digest": d }))
203 .collect();
204 nb.insert("covers".into(), Value::Array(list));
205 }
206 vec![
207 node_add("note", id.clone(), Value::Object(nb)),
208 edge_add("about", &id, about, new_id),
209 ]
210}
211
212pub fn confirm(page: &PageView, anchors: &[Anchor], verified_at_ms: u64) -> Vec<EventSpec> {
217 let value: Vec<Value> = anchors.iter().map(anchor_value).collect();
218 let exempt = |field: &str, value: Value| EventSpec {
219 op: "field.set",
220 target: page.page.id.clone(),
221 node_type: None,
222 body: obj(json!({ "field": field, "value": value })),
223 base_event: None,
224 };
225 let mut anchors = field_set(
226 &page.page.id,
227 "anchors",
228 Value::Array(value),
229 &page.page.head,
230 );
231 anchors
232 .body
233 .insert("verified_at".into(), Value::from(verified_at_ms));
234 vec![
235 anchors,
236 exempt("anchor_state", Value::from("fresh")),
237 exempt("drift", Value::Array(Vec::new())),
238 ]
239}
240
241pub fn retract(target: &str, reason: &str) -> EventSpec {
243 EventSpec {
244 op: "retract",
245 target: target.to_owned(),
246 node_type: None,
247 body: obj(json!({ "reason": reason })),
248 base_event: None,
249 }
250}
251
252pub fn revise_page(
258 page: &PageView,
259 markdown: &str,
260 links_now: &[(String, String)],
261 links_want: &[String],
262 new_id: NewId<'_>,
263) -> Vec<EventSpec> {
264 let (title, wanted) = sections::split(markdown);
265 let mut out = Vec::new();
266 if !title.is_empty() && title != page.title() {
267 out.push(field_set(
268 &page.page.id,
269 "title",
270 Value::from(title),
271 &page.page.head,
272 ));
273 }
274 let old: Vec<&str> = page.sections.iter().map(|s| s.body.as_str()).collect();
275 let new: Vec<&str> = wanted.iter().map(|s| s.text.as_str()).collect();
276 let plan = align(&old, &new);
279 let mut orders: Vec<Option<u32>> = plan
280 .iter()
281 .map(|step| match step {
282 Step::Keep(i) | Step::Edit(i) => Some(page.sections[*i].order),
283 Step::Add => None,
284 })
285 .collect();
286 let renumber = !fill_orders(&mut orders);
287 let fresh = sections::renumber(plan.len());
288 for (k, step) in plan.iter().enumerate() {
289 let order = if renumber {
290 fresh[k]
291 } else {
292 orders[k].unwrap_or(fresh[k])
293 };
294 match step {
295 Step::Keep(i) | Step::Edit(i) => {
296 let s = &page.sections[*i];
297 if matches!(step, Step::Edit(_)) {
298 out.push(edit_section(s, new[k]));
299 }
300 if order != s.order {
301 out.push(field_set(&s.id, "order", Value::from(order), &s.head));
302 }
303 }
304 Step::Add => {
305 let sid = new_id("n");
306 out.push(node_add(
307 "section",
308 sid.clone(),
309 json!({ "body": new[k], "order": order }),
310 ));
311 out.push(edge_add("part_of", &sid, &page.page.id, new_id));
312 }
313 }
314 }
315 let used: std::collections::BTreeSet<usize> = plan
316 .iter()
317 .filter_map(|s| match s {
318 Step::Keep(i) | Step::Edit(i) => Some(*i),
319 Step::Add => None,
320 })
321 .collect();
322 for (i, s) in page.sections.iter().enumerate() {
323 if !used.contains(&i) {
324 out.push(retract(&s.id, "removed from the page"));
325 }
326 }
327 for (edge, to) in links_now {
328 if !links_want.contains(to) {
329 out.push(retract(edge, "the page no longer links there"));
330 }
331 }
332 for to in links_want {
333 if !links_now.iter().any(|(_, t)| t == to) && *to != page.page.id {
334 out.push(edge_add("links_to", &page.page.id, to, new_id));
335 }
336 }
337 out
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341enum Step {
342 Keep(usize),
343 Edit(usize),
344 Add,
345}
346
347fn align(old: &[&str], new: &[&str]) -> Vec<Step> {
351 let (n, m) = (old.len(), new.len());
352 let mut lcs = vec![vec![0u32; m + 1]; n + 1];
353 for i in (0..n).rev() {
354 for j in (0..m).rev() {
355 lcs[i][j] = if old[i] == new[j] {
356 lcs[i + 1][j + 1] + 1
357 } else {
358 lcs[i + 1][j].max(lcs[i][j + 1])
359 };
360 }
361 }
362 let mut steps = Vec::with_capacity(m);
363 let (mut i, mut j) = (0, 0);
364 let mut spare: Vec<usize> = Vec::new();
365 while j < m {
366 if i < n && old[i] == new[j] {
367 spare.clear();
368 steps.push(Step::Keep(i));
369 i += 1;
370 j += 1;
371 } else if i < n && lcs[i + 1][j] >= lcs[i][j + 1] {
372 spare.push(i);
373 i += 1;
374 } else {
375 steps.push(if spare.is_empty() {
376 Step::Add
377 } else {
378 Step::Edit(spare.remove(0))
379 });
380 j += 1;
381 }
382 }
383 steps
384}
385
386fn fill_orders(orders: &mut [Option<u32>]) -> bool {
389 let mut k = 0;
390 let mut prev: Option<u32> = None;
391 while k < orders.len() {
392 if let Some(o) = orders[k] {
393 if prev.is_some_and(|p| o <= p) {
394 return false;
395 }
396 prev = Some(o);
397 k += 1;
398 continue;
399 }
400 let start = k;
401 while k < orders.len() && orders[k].is_none() {
402 k += 1;
403 }
404 let next = orders.get(k).copied().flatten();
405 let lo = prev.unwrap_or(0);
406 let count = (k - start) as u32;
407 let Some(span) = next.map_or(Some(crate::ORDER_GAP.saturating_mul(count + 1)), |hi| {
408 hi.checked_sub(lo)
409 }) else {
410 return false;
411 };
412 let step = span / (count + 1);
413 if step == 0 {
414 return false;
415 }
416 for (n, slot) in orders[start..k].iter_mut().enumerate() {
417 *slot = Some(lo + step * (n as u32 + 1));
418 }
419 prev = orders[k - 1];
420 }
421 true
422}
423
424pub fn chain(specs: &mut [EventSpec], event_ids: &[String]) {
428 let mut last: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
429 for (spec, id) in specs.iter_mut().zip(event_ids) {
430 if spec.base_event.is_some() {
431 if let Some(prev) = last.get(&spec.target) {
432 spec.base_event = Some(prev.clone());
433 }
434 }
435 if spec.op != "edge.add" {
436 last.insert(spec.target.clone(), id.clone());
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::replica::{LogEvent, assemble};
445
446 fn ids() -> impl FnMut(&str) -> String {
447 let mut n = 0;
448 move |p: &str| {
449 n += 1;
450 format!("{p}_{n:026}")
451 }
452 }
453
454 fn apply(specs: &[EventSpec], first: u64) -> Vec<LogEvent> {
456 specs
457 .iter()
458 .enumerate()
459 .map(|(i, s)| LogEvent {
460 seq: first + i as u64,
461 event_id: format!("ev{}", first + i as u64),
462 op: s.op.into(),
463 target: Some(s.target.clone()),
464 node_type: s.node_type.map(Into::into),
465 body: Some(Value::Object(s.body.clone())),
466 })
467 .collect()
468 }
469
470 fn page() -> Page {
471 let (title, secs) = sections::split("# Start\nintro\n## One\na\n## Two\nb\n");
472 Page {
473 slug: "guide/start".into(),
474 title,
475 kind: "guide".into(),
476 status: PageStatus::Draft,
477 sections: secs,
478 anchors: Vec::new(),
479 notes: Vec::new(),
480 }
481 }
482
483 #[test]
484 fn a_created_page_assembles_back_to_its_markdown() {
485 let mut new_id = ids();
486 let (specs, id) = create_page(&page(), &[], &mut new_id);
487 assert_eq!(specs.len(), 1 + 2 * 3);
488 let space = assemble(&apply(&specs, 1));
489 let p = &space.pages[&id];
490 assert_eq!(p.markdown(), "# Start\nintro\n## One\na\n## Two\nb\n");
491 }
492
493 #[test]
494 fn edits_and_inserts_keep_the_order_and_name_their_base() {
495 let mut new_id = ids();
496 let (specs, id) = create_page(&page(), &[], &mut new_id);
497 let mut log = apply(&specs, 1);
498 let space = assemble(&log);
499 let p = &space.pages[&id];
500 let edit = edit_section(&p.sections[1], "## One\nA!\n");
501 assert_eq!(
502 edit.base_event.as_deref(),
503 Some(p.sections[1].head.as_str())
504 );
505 let ins = insert_section(p, Some(0), "## Half\nh\n", &mut new_id);
506 let mut batch = vec![edit];
507 batch.extend(ins);
508 log.extend(apply(&batch, 100));
509 let p = &assemble(&log).pages[&id];
510 assert_eq!(
511 p.markdown(),
512 "# Start\nintro\n## Half\nh\n## One\nA!\n## Two\nb\n"
513 );
514 }
515
516 #[test]
517 fn a_full_gap_renumbers_before_the_insert() {
518 let mut new_id = ids();
519 let (mut specs, id) = create_page(&page(), &[], &mut new_id);
520 if let Some(b) = specs
522 .iter_mut()
523 .find(|s| s.body.get("order") == Some(&Value::from(2048)))
524 {
525 b.body.insert("order".into(), Value::from(1025));
526 }
527 let mut log = apply(&specs, 1);
528 let p = assemble(&log).pages[&id].clone();
529 let ins = insert_section(&p, Some(0), "## Mid\nm\n", &mut new_id);
530 assert!(ins.iter().filter(|s| s.op == "field.set").count() >= 2);
531 log.extend(apply(&ins, 100));
532 let p = &assemble(&log).pages[&id];
533 assert_eq!(
534 p.markdown(),
535 "# Start\nintro\n## Mid\nm\n## One\na\n## Two\nb\n"
536 );
537 }
538
539 fn revised(old: &str, new: &str) -> (Vec<EventSpec>, String) {
540 let mut new_id = ids();
541 let (title, secs) = sections::split(old);
542 let p = Page {
543 title,
544 sections: secs,
545 ..page()
546 };
547 let (specs, id) = create_page(&p, &[], &mut new_id);
548 let mut log = apply(&specs, 1);
549 let view = assemble(&log).pages[&id].clone();
550 let rev = revise_page(&view, new, &[], &[], &mut new_id);
551 log.extend(apply(&rev, 100));
552 (rev, assemble(&log).pages[&id].markdown())
553 }
554
555 #[test]
556 fn revise_keeps_unchanged_sections_and_edits_only_what_moved() {
557 let old = "# T\nlead\n## A\na\n## B\nb\n## C\nc\n";
558 let new = "# T\nlead\n## A\na\n## B\nB changed\n## C\nc\n";
559 let (rev, md) = revised(old, new);
560 assert_eq!(md, new);
561 assert_eq!(rev.len(), 1, "one edit: {rev:?}");
562 assert_eq!(rev[0].op, "field.set");
563 }
564
565 #[test]
566 fn revise_inserts_removes_and_retitles() {
567 let old = "# T\nlead\n## A\na\n## B\nb\n";
568 let new = "# New\nlead\n## X\nx\n## B\nb\n## Z\nz\n";
569 let (rev, md) = revised(old, new);
570 assert_eq!(md, new);
571 assert!(
572 rev.iter()
573 .any(|s| s.body.get("field") == Some(&Value::from("title")))
574 );
575 assert!(
576 !rev.iter().any(|s| s.op == "retract"),
577 "A became X by an edit"
578 );
579 let (rev, md) = revised(old, "# T\nlead\n## B\nb\n");
580 assert_eq!(md, "# T\nlead\n## B\nb\n");
581 assert_eq!(rev.iter().filter(|s| s.op == "retract").count(), 1);
582 }
583
584 #[test]
585 fn revise_renumbers_when_inserts_do_not_fit() {
586 let old = "# T\nlead\n## A\na\n";
587 let many = (0..1100).fold(String::new(), |mut acc, i| {
588 acc.push_str("## S");
589 acc.push_str(&i.to_string());
590 acc.push_str("\ns\n");
591 acc
592 });
593 let new = format!("# T\nlead\n{many}## A\na\n");
594 let (_, md) = revised(old, &new);
595 assert_eq!(md, new);
596 }
597
598 #[test]
599 fn revise_keeps_links_to_in_step() {
600 let mut new_id = ids();
601 let (specs, id) = create_page(&page(), &[], &mut new_id);
602 let view = assemble(&apply(&specs, 1)).pages[&id].clone();
603 let now = vec![
604 ("e_old".to_owned(), "n_gone".to_owned()),
605 ("e_keep".to_owned(), "n_b".to_owned()),
606 ];
607 let rev = revise_page(
608 &view,
609 &view.markdown(),
610 &now,
611 &["n_b".into(), "n_c".into()],
612 &mut new_id,
613 );
614 assert_eq!(rev.len(), 2);
615 assert_eq!((rev[0].op, rev[0].target.as_str()), ("retract", "e_old"));
616 assert_eq!(rev[1].body.get("to"), Some(&Value::from("n_c")));
617 }
618
619 #[test]
620 fn chain_names_the_previous_write_of_the_same_node() {
621 let mut specs = vec![
622 field_set("n_a", "body", Value::from("1"), "h0"),
623 field_set("n_a", "body", Value::from("2"), "h0"),
624 field_set("n_b", "body", Value::from("x"), "hb"),
625 ];
626 chain(&mut specs, &["e1".into(), "e2".into(), "e3".into()]);
627 let bases: Vec<_> = specs
628 .iter()
629 .map(|s| s.base_event.clone().unwrap_or_default())
630 .collect();
631 assert_eq!(bases, ["h0", "e1", "hb"]);
632 }
633
634 #[test]
635 fn confirm_sends_anchors_with_base_and_exempt_verdicts() {
636 let mut new_id = ids();
637 let (specs, id) = create_page(&page(), &[], &mut new_id);
638 let p = assemble(&apply(&specs, 1)).pages[&id].clone();
639 let c = confirm(&p, &[], 1_792_800_020_000);
640 assert_eq!(c[0].base_event.as_deref(), Some(p.page.head.as_str()));
641 assert_eq!(c[0].body["verified_at"], 1_792_800_020_000_u64);
642 assert!(c[1].base_event.is_none() && c[2].base_event.is_none());
643 }
644}