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