oficina / docs Public

Note where the first valued mark's allowlist has to go

mark_names returns names and drops every value, which is why an unknown
mark carried through by name is safe today. A valued mark changes that,
and the tempting allowlist — link is the one with an href — is wrong on
arrival: this function collects marks it has never heard of straight off
the wire, and POST /d/{sqid}/updates accepts bytes that never met the
schema, so an unknown mark can carry a value too.

It is also the right place: the single point the HTML renderer and every
JSON consumer agree on what marks a run carries, so a check here cannot
be re-implemented differently by markdown.go and pdf.go.

Found by the session that owns Plan 7, reading this file after the
point-ladder work restructured around it. Recorded here rather than only
in that plan, because the plan may land long after someone else touches
this function.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev ca9641b22639fb4aa05e743396b7530111c436c0 parent 250db1c
1 file changed, +29 −0
  • core/src/fragment.rs +29 −0

Intent

Attributed to this commit: 526 g CO₂e (60804241 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on diagnose, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.

diff --git a/core/src/fragment.rs b/core/src/fragment.rs
index 5074ad3..3e18e97 100644
--- a/core/src/fragment.rs
+++ b/core/src/fragment.rs
@@ -517,6 +517,35 @@ fn escape_json(s: &str, out: &mut String) {
/// children (see the module comment): a consumer that ignores a mark it
/// does not recognise still has the text, and one that grows to
/// understand it needs it to have been there.
+/// WHOEVER ADDS THE FIRST VALUED MARK: THIS FUNCTION IS WHERE THE
+/// ALLOWLIST GOES, AND IT MUST NOT ASSUME ONLY THAT MARK HAS A VALUE.
+///
+/// This returns NAMES. Every mark's value — including an unknown mark's
+/// — is dropped here and reaches no consumer, which is why an unknown
+/// mark carried through by name is safe today: `fragment_html` finds no
+/// MARKS row and emits no wrapper, and `text_json` writes the name
+/// through `escape_json`.
+///
+/// A valued mark changes that. The moment one value is allowed past
+/// this point, the shape of the question becomes "which marks may carry
+/// a value, and what may that value be" — and the tempting answer, that
+/// `link` is the one with an `href`, is wrong on arrival. This function
+/// collects marks it has never heard of, straight off the wire, and
+/// `POST /d/{sqid}/updates` accepts bytes that never went through
+/// ProseMirror's schema. So an unknown mark can carry an arbitrary
+/// value too, and an allowlist written around the known one would let
+/// it past.
+///
+/// It is also the right place, and that is the other half of the note:
+/// this is the single point the HTML renderer and every JSON consumer
+/// agree on what marks a run carries, so a check here cannot be
+/// re-implemented differently by `markdown.go` and `pdf.go` the way one
+/// in `node_html` would have to be. Plan 7's D1 names it.
+///
+/// (Found by the session that owns Plan 7, reading this file after the
+/// point-ladder work restructured around it — recorded here rather than
+/// only in that plan, because the plan may land long after someone else
+/// touches this function.)
fn mark_names(attrs: &yrs::types::Attrs) -> Vec<String> {
let mut names: Vec<String> = MARKS
.iter()