Skip to content
C.W.K.
Lesson 05 of 05 · published

Anchored Notes, Revisions as New Versions

~13 min · anchoring, revisions, lineage, append-only, ui-marking

Level 0First Word
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

An anchor is an exact quote

The reviewer was told to copy each passage character for character, and the engine takes that literally. The first code block records a note only when it has both a quote and a comment, and computes anchored with one plain test: is the quote a substring of the section text the reviewer read? First the quote loses its leading and trailing whitespace, and that trimmed quote is both what is stored and what is searched for. Beyond that trim there is no fuzzy matching and no normalizing. A reviewer that paraphrases instead of quoting still produces a note, which is kept, shown, and flagged as not found: the web adds (not found in the text), the phone (not found). The first version test pins both outcomes with one note whose quote is in the version and one whose quote is not.

Keeping the unanchored note is a choice. The comment may be right even when the quote was copied badly, and dropping the note would hide a reviewer's insight along with its mistake. The flag lets the reader weigh it.

Marked where it points

Dad reads a version with its notes painted onto it. Both clients join the version's sections with a blank line and pass that text, with every note on the version, to markNotes: the web's lives in frontend/src/momentState.ts (second code block), and the phone's Swift twin in MomentState.swift names the web's in its doc comment. The function finds each quote, keeps a list of spans, and cuts the text into plain runs and marked runs. Two notes on exactly the same passage share one span. A note whose passage overlaps a span already taken gets no mark of its own, though it stays in the review's list. The result is a flat list of runs any renderer can draw. The web wraps marked runs in a <mark> element with the comments as its tooltip, and the phone paints an attributed string. A suspect note turns its run red, and naturalness notes stay amber.

Revise: the same seat writes again

Once the notes are in, Dad can press Revise with notes. The route, POST /api/moments/{id}/versions/{turn}/revise, answers 409 while the version is still being written. Otherwise it opens a new version turn copied from the earlier one: the same brain, effort and model, the same purpose and language, the same job and seat number, the same reviewer list, and a revises_turn pointing back. Both clients disable the button while the version has no notes, or while a review or another revision of it is running.

run_version then runs exactly as in Lesson 2, with two more inputs for each section, gathered by the code you saw there: the earlier version's translation of that section, and every note a reviewer left on that section of the earlier version. The third code block shows how version_prompt lays them out. The instruction attached to the notes is the heart of this track: fix what they rightly flag, and where a note is wrong about the meaning, keep what the source says.

That sentence closes the loop Lesson 4 opened. The reviewer flagged passages without the source, and the writer judges them with the source in hand. A suspect note can be right, and the writer fixes the passage. It can also be wrong, because a faithful translation of an odd original can read oddly, and then the source wins.

Nothing is overwritten

A revision is a new version beside the old one. The earlier turn and its sections stay exactly as they were, and the revision test asserts that the turns holding sections are the earlier one and the revision, both. The revision wears a revision chip on both clients, and because it copied the reviewer list, its own cold reviews follow when it finishes. Dad can revise a revision too. Every step stays readable, side by side, in the moment's record.

Lineage beside the seat, never over it

One small field decision keeps revisions honest. When a version finishes, run_version records answered_model and answered_effort from the last envelope, beside the seat's own model and effort, and never writes over them. The comment gives two reasons. The seat's own model is often none, which means the vessel's default, and that is what a revision should ask for again. And a model id a vessel reports is not always one the vessel accepts back as a request.

The revision test checks both halves: the revision's call goes out with model still None, and the earlier turn holds model as None beside an answered_model of m-1. The web shows the answered model next to the seat label, so Dad sees what ran, and the record still remembers what he chose.

Principle: Keep what was asked for and what answered as separate fields, and let a redo ask again with what was asked for. When the reviewer cannot see the source, the final word on meaning belongs to the writer who can.

Inkwell Quest tells the same story from the author's side, in its lessons on the reviewer who holds no pen and the author who may stet.

Code

cwkSpark, backend/versions.py: notes recorded with their quote and an anchored flag, and a verdict per section (verbatim, lines 413-441)·python
                found = 0
                for obj in _review_objects(str(envelope.get("result") or "")):
                    kind = str(obj.get("type") or "")
                    if kind == "note" and str(obj.get("quote") or "").strip() and str(obj.get("comment") or "").strip():
                        quote = str(obj["quote"]).strip()
                        await self._write(  # type: ignore[attr-defined]
                            self.corpus.add_result,  # type: ignore[attr-defined]
                            moment["id"],
                            turn_id=turn["id"],
                            target_language=language,
                            fields={
                                "kind": "note",
                                "n": n,
                                "about": version_id,
                                "quote": quote,
                                "anchored": quote in text,
                                "note_kind": "suspect" if obj.get("kind") == "suspect" else "naturalness",
                                "comment": str(obj["comment"]).strip(),
                            },
                        )
                        found += 1
                    elif kind == "verdict" and str(obj.get("note") or "").strip():
                        await self._write(  # type: ignore[attr-defined]
                            self.corpus.add_result,  # type: ignore[attr-defined]
                            moment["id"],
                            turn_id=turn["id"],
                            target_language=language,
                            fields={"kind": "verdict", "n": n, "about": version_id, "note": str(obj["note"]).strip()},
                        )
cwkSpark, frontend/src/momentState.ts: a version's text cut into plain runs and marked runs (verbatim, lines 225-254)·typescript
/** A version's text cut into plain runs and runs a note quotes, so each
 * anchored note can be marked where it points. Overlapping quotes keep the
 * earlier one; a quote not found stays in the list without a mark. */
export function markNotes(text: string, notes: Result[]): { text: string; notes: Result[] }[] {
  const spans: { start: number; end: number; notes: Result[] }[] = [];
  for (const note of notes) {
    const quote = note.quote ?? "";
    if (!quote) continue;
    const start = text.indexOf(quote);
    if (start < 0) continue;
    const end = start + quote.length;
    const same = spans.find((span) => span.start === start && span.end === end);
    if (same) {
      same.notes.push(note);
      continue;
    }
    if (spans.some((span) => start < span.end && end > span.start)) continue;
    spans.push({ start, end, notes: [note] });
  }
  spans.sort((a, b) => a.start - b.start);
  const out: { text: string; notes: Result[] }[] = [];
  let at = 0;
  for (const span of spans) {
    if (span.start > at) out.push({ text: text.slice(at, span.start), notes: [] });
    out.push({ text: text.slice(span.start, span.end), notes: span.notes });
    at = span.end;
  }
  if (at < text.length) out.push({ text: text.slice(at), notes: [] });
  return out;
}
cwkSpark, backend/versions.py: a section's prompt, with the earlier version and the notes when it is a revision (verbatim, lines 106-135)·python
def version_prompt(
    n: int,
    total: int,
    section: str,
    *,
    glossary: list[tuple[str, str]],
    previous: tuple[str, str] | None,
    earlier: str | None = None,
    notes: list[dict[str, Any]] | None = None,
) -> str:
    parts = [f"Section {n} of {total}."]
    if glossary:
        parts.append("Glossary so far (binding):\n" + "\n".join(f"- {s} → {t}" for s, t in glossary))
    if previous is not None:
        parts.append(
            "The section before this one, for continuity (do not translate it again):\n"
            f"<source>\n{previous[0]}\n</source>\n<translation>\n{previous[1]}\n</translation>"
        )
    if earlier is not None:
        parts.append(f"Your earlier version of this section:\n<translation>\n{earlier}\n</translation>")
    if notes:
        lines = "\n".join(
            f"- [{note.get('note_kind') or 'note'}] “{note.get('quote')}”: {note.get('comment')}" for note in notes
        )
        parts.append(
            "Notes from reviewers who read only the earlier version, never the source. Fix what they rightly "
            "flag; where a note is wrong about the meaning, keep what the source says:\n" + lines
        )
    parts.append(f"Translate this section:\n<source>\n{section}\n</source>")
    return "\n\n".join(parts)

External links

Exercise

Give a document editor or a generation pipeline you are building anchored review notes and revisions. Store each note with an exact quote, the section it belongs to and an anchored flag computed by a plain substring test. Render the text as plain and marked runs, one span per passage, with overlapping notes kept in the list but unmarked. Then add a revise action that creates a new version from the same settings, passes the earlier text and its notes to the writer, and leaves the earlier version untouched.
Hint
Search for each quote only inside the section the note names, so a repeated phrase is marked where the reviewer meant it. Store the requested model and the model that answered as two fields, and test that the revise call sends the requested one. Finally, assert that after a revision both versions still exist with their own text.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.