Skip to main content

lean_ctx_doc/
write.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Doc writes as unsigned place events (C0 2.2). The engine fills in the
4//! envelope (event id, nonce, commit, HLC, signature) and queues them in
5//! order; CAS-checked `field.set`s carry the head they were built on, so a
6//! batch can chain edits: each event's base is the one before it.
7
8use 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/// One event to sign and queue.
16#[derive(Debug, Clone, PartialEq)]
17pub struct EventSpec {
18    /// `node.add`, `field.set`, `edge.add` or `retract`.
19    pub op: &'static str,
20    pub target: String,
21    /// For `node.add`: `page`, `section` or `note`.
22    pub node_type: Option<&'static str>,
23    /// The body without `body_nonce`.
24    pub body: Map<String, Value>,
25    /// The head this write was built on (CAS-checked fields only).
26    pub base_event: Option<String>,
27}
28
29/// Makes ids: `"n"` for nodes, `"e"` for edges (`n_…`, `e_…`).
30pub 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
39/// The wire form of an anchor: `{ref, digest, commit, mode?, role?}`.
40pub 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
82/// The events that create `page`: the page node, its sections with their
83/// `part_of` edges, its notes with their `about` edges (to the page) and a
84/// `links_to` edge per linked page id. Bodies must already be in storage
85/// form ([`crate::links::to_storage`]). Returns the events and the page id.
86pub 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
134/// A CAS-checked `field.set` of `field` on a node whose head is `base`.
135pub 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
148/// Replaces the body of `section`.
149pub fn edit_section(section: &SectionView, body: &str) -> EventSpec {
150    field_set(&section.id, "body", Value::from(body), &section.head)
151}
152
153/// Inserts a section with `text` after the section at `after` (`None`: at
154/// the front). When no order fits between the neighbours the page is
155/// renumbered first, one CAS `field.set order` per section.
156pub 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        // No gap left: every section moves to `(i + 1) * ORDER_GAP`, the new
169        // one taking the slot at `next`.
170        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
189/// A note about `about` (a page or section id); `covers` are `(ref, digest)`
190/// pairs the note accounts for.
191pub 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
212/// Confirms a page against the checkout: new anchors (CAS), then the
213/// machine verdicts `anchor_state: fresh` and an empty `drift` (exempt).
214/// A re-anchor carries `verified_at` (ms) in its body: C0 refuses one
215/// without it.
216pub 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
241/// `retract` of a node or an edge with `reason` (2–500 characters).
242pub 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
252/// The events that turn `page` into `markdown` (storage form: links
253/// already `hive:`). Unchanged sections stay; a changed stretch becomes edits
254/// of the sections it replaces, then inserts or retractions for the rest. A
255/// new `# ` title is a CAS `field.set title`. `links_want` are the page ids
256/// the new body links to; `links_now` the live `links_to` edges.
257pub 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    // Each new section either keeps an old one (same text, `Keep`), takes an
277    // old one's place (`Edit`) or is added (`Add`); unmatched old ones go.
278    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
347/// Aligns `new` sections to `old` ones: the longest common subsequence of
348/// equal texts is kept; between two kept anchors the old sections are
349/// re-used in order for edits, the rest added or dropped.
350fn 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
386/// Fills the `None` orders strictly between their known neighbours; false
387/// when some gap is too small (or the known orders are not increasing).
388fn 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
424/// Chains the bases of a batch: a CAS write on a node another event of the
425/// batch already wrote names that event, so the batch applies in order.
426/// `event_ids` are the ids the engine gave each event, in batch order.
427pub 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    /// Applies specs as if the server accepted them, `seq` from `first`.
455    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        // Squeeze the first two sections together: no integer fits between.
521        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}