use std::borrow::Cow;
use maud::html;
use serde_json::Value;
use crate::{
render::Scribe,
tools::{Setting, plain, printed, prose},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlossKind {
Hook,
Rule,
Skill,
Command,
Plan,
Note,
}
impl GlossKind {
pub fn label(self) -> &'static str {
match self {
GlossKind::Hook => "hook",
GlossKind::Rule => "rule",
GlossKind::Skill => "skill",
GlossKind::Command => "command",
GlossKind::Plan => "plan",
GlossKind::Note => "note",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Body {
Prose(String),
Printed(String),
Plain(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Firing {
event: String,
command: Option<String>,
}
impl Firing {
fn joins(&self, other: &Firing) -> bool {
self.event == other.event
&& match (&self.command, &other.command) {
(Some(mine), Some(theirs)) => mine == theirs,
_ => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gloss {
pub kind: GlossKind,
pub gist: Option<String>,
pub notes: Vec<String>,
pub body: Vec<Body>,
pub firing: Option<Firing>,
}
impl Gloss {
fn new(kind: GlossKind) -> Self {
Self {
kind,
gist: None,
notes: Vec::new(),
body: Vec::new(),
firing: None,
}
}
fn firing(mut self, event: Option<&str>, command: Option<&str>) -> Self {
self.firing = event.map(|event| Firing {
event: event.to_owned(),
command: command.map(str::to_owned),
});
self
}
pub(crate) fn absorb(&mut self, other: Gloss) {
if self.gist.is_none() {
self.gist = other.gist;
}
self.body.extend(other.body);
for note in other.notes {
if !self.notes.contains(¬e) {
self.notes.push(note);
}
}
if let (Some(firing), Some(joined)) = (&mut self.firing, other.firing)
&& firing.command.is_none()
{
firing.command = joined.command;
}
}
pub(crate) fn ran_by(&mut self, command: &str) {
self.kind = GlossKind::Skill;
if self.body.is_empty()
&& let Some(said) = self.gist.take()
{
self.body.push(Body::Prose(said));
}
self.gist = Some(command.trim_start_matches('/').to_owned());
}
pub(crate) fn same_firing_as(&self, other: &Gloss) -> bool {
self.kind == GlossKind::Hook
&& other.kind == GlossKind::Hook
&& match (&self.firing, &other.firing) {
(Some(mine), Some(theirs)) => mine.joins(theirs),
_ => false,
}
}
fn gist(mut self, gist: impl Into<String>) -> Self {
self.gist = Some(gist.into());
self
}
fn maybe_gist(self, gist: Option<impl Into<String>>) -> Self {
match gist {
Some(gist) => self.gist(gist),
None => self,
}
}
fn note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
fn maybe_note(self, note: Option<impl Into<String>>) -> Self {
match note {
Some(note) => self.note(note),
None => self,
}
}
fn prose(mut self, body: impl Into<String>) -> Self {
self.body.push(Body::Prose(body.into()));
self
}
fn printed(mut self, body: impl Into<String>) -> Self {
self.body.push(Body::Printed(body.into()));
self
}
fn plain(mut self, body: impl Into<String>) -> Self {
self.body.push(Body::Plain(body.into()));
self
}
fn maybe_plain(self, body: Option<impl Into<String>>) -> Self {
match body {
Some(body) => self.plain(body),
None => self,
}
}
fn maybe_prose(self, body: Option<impl Into<String>>) -> Self {
match body {
Some(body) => self.prose(body),
None => self,
}
}
fn is_bare(&self) -> bool {
self.gist.is_none() && self.body.is_empty()
}
}
pub fn setting(scribe: &Scribe, gloss: &Gloss) -> Setting {
let setting = Setting::new().maybe_gist(gloss.gist.as_deref());
let setting = gloss
.notes
.iter()
.fold(setting, |setting, note| setting.note(note.as_str()));
if gloss.body.is_empty() {
return setting;
}
setting.body(html! {
@for body in &gloss.body {
(match body {
Body::Prose(text) => prose(scribe, text),
Body::Printed(text) => printed(scribe, text),
Body::Plain(text) => plain(scribe, text),
})
}
})
}
pub fn attachment(attachment: &Value) -> Option<Gloss> {
let gloss = match text(attachment, "type")? {
"hook_success" => hook_output(attachment)?,
"hook_system_message" => Gloss::new(GlossKind::Hook)
.firing(text(attachment, "toolUseID"), None)
.maybe_gist(text(attachment, "content"))
.maybe_note(text(attachment, "hookName")),
"hook_additional_context" => Gloss::new(GlossKind::Hook)
.firing(text(attachment, "toolUseID"), None)
.maybe_gist(text(attachment, "hookName"))
.note("added context")
.printed(paragraphs(attachment.get("content")?)?),
"hook_cancelled" => Gloss::new(GlossKind::Hook)
.firing(text(attachment, "toolUseID"), text(attachment, "command"))
.maybe_gist(text(attachment, "hookName"))
.maybe_note(text(attachment, "command"))
.note(
if flag(attachment, "timedOut") {
"timed out"
} else {
"cancelled"
},
),
"nested_memory" => Gloss::new(GlossKind::Rule)
.maybe_gist(subject(attachment))
.maybe_note(text(attachment.get("content")?, "type").map(str::to_lowercase))
.prose(text(attachment.get("content")?, "content")?),
"plan_mode" => plan_entered(attachment)?,
"plan_mode_exit" => plan_left(
attachment,
if flag(attachment, "planExists") {
"left, plan written"
} else {
"left, no plan written"
},
),
"edited_text_file" => Gloss::new(GlossKind::Note)
.maybe_gist(text(attachment, "filename"))
.note("edited outside the session")
.plain(text(attachment, "snippet")?),
"file" => Gloss::new(GlossKind::Note)
.maybe_gist(subject(attachment))
.note("attached")
.prose(text(attachment.get("content")?.get("file")?, "content")?),
"directory" => Gloss::new(GlossKind::Note)
.maybe_gist(subject(attachment))
.note("attached")
.plain(text(attachment, "content")?),
"read_truncation_notice" => Gloss::new(GlossKind::Note)
.gist("read truncated")
.plain(text(attachment, "banner")?),
"goal_status" => Gloss::new(GlossKind::Note)
.maybe_gist(text(attachment, "condition"))
.note(
if flag(attachment, "met") {
"met"
} else {
"not met"
},
),
_ => return None,
};
(!gloss.is_bare()).then_some(gloss)
}
fn hook_output(attachment: &Value) -> Option<Gloss> {
let contributed = text(attachment, "content")
.or_else(|| text(attachment, "stdout").filter(|stdout| !is_protocol(stdout)))
.map(str::trim)
.filter(|contributed| !contributed.is_empty());
let printed = text(attachment, "stderr")
.map(str::trim)
.filter(|printed| !printed.is_empty());
let said: Vec<&str> = contributed.into_iter().chain(printed).collect();
if said.is_empty() {
return None;
}
let exit = attachment.get("exitCode").and_then(Value::as_i64);
Some(
Gloss::new(GlossKind::Hook)
.firing(text(attachment, "toolUseID"), text(attachment, "command"))
.maybe_gist(text(attachment, "hookName"))
.maybe_note(text(attachment, "command"))
.maybe_note(
exit.filter(|code| *code != 0)
.map(|code| format!("exit {code}")),
)
.plain(said.join("\n")),
)
}
const HARNESS_CONTROLS: &[&str] = &[
"/copy",
"/skills",
"/agents",
"/login",
"/logout",
"/resume",
"/config",
"/status",
"/cost",
"/doctor",
"/help",
"/terminal-setup",
];
fn controls_the_harness(name: &str) -> bool {
HARNESS_CONTROLS.contains(&name)
}
fn is_protocol(stdout: &str) -> bool {
serde_json::from_str::<Value>(stdout.trim()).is_ok_and(|value| {
value.get("systemMessage").is_some() || value.get("hookSpecificOutput").is_some()
})
}
fn plan_left(attachment: &Value, state: &str) -> Gloss {
match text(attachment, "planFilePath") {
Some(path) => Gloss::new(GlossKind::Plan).gist(path).note(state),
None => Gloss::new(GlossKind::Plan).gist(state),
}
}
fn plan_entered(attachment: &Value) -> Option<Gloss> {
(text(attachment, "reminderType") == Some("full")).then(|| {
Gloss::new(GlossKind::Plan)
.gist("entered plan mode")
.maybe_note(flag(attachment, "isSubAgent").then_some("subagent"))
})
}
pub fn system(system: &Value) -> Option<Gloss> {
let said = text(system, "content").map(str::trim).unwrap_or_default();
let gloss = match text(system, "subtype")? {
"local_command" => {
let printed = unwrap_tag(said, "local-command-stdout")?.trim();
(!printed.is_empty())
.then(|| Gloss::new(GlossKind::Command).gist("output").plain(printed))?
}
"model_refusal_fallback" => Gloss::new(GlossKind::Note)
.gist("model switched")
.maybe_note(
text(system, "originalModel")
.zip(text(system, "fallbackModel"))
.map(|(from, to)| format!("{from} → {to}")),
)
.prose(said),
"informational" | "away_summary" => told(GlossKind::Note, said),
_ => return None,
};
(!gloss.is_bare()).then_some(gloss)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Wrapped {
Note(Gloss),
Nothing,
}
pub fn wrapped(said: &str, is_meta: bool) -> Option<Wrapped> {
let said = strip_tag(said, "local-command-caveat");
let said = said.trim();
if let Some(wrapped) = command(said) {
return Some(wrapped);
}
if !is_meta {
return None;
}
Some(match meta(said) {
Some(gloss) => Wrapped::Note(gloss),
None => Wrapped::Nothing,
})
}
fn meta(said: &str) -> Option<Gloss> {
const SKILL_OPENING: &str = "Base directory for this skill:";
const IMAGE_SCALING: (&str, &str) = ("[Image: original ", "Multiply coordinates");
if said.is_empty() || (said.starts_with(IMAGE_SCALING.0) && said.contains(IMAGE_SCALING.1)) {
return None;
}
let Some(rest) = said.strip_prefix(SKILL_OPENING) else {
return Some(told(GlossKind::Note, said));
};
let (directory, instructions) = rest.trim_start().split_once('\n').unwrap_or((rest, ""));
Some(
Gloss::new(GlossKind::Skill)
.maybe_gist(directory.trim().rsplit('/').next())
.maybe_prose(Some(instructions.trim_start()).filter(|body| !body.is_empty())),
)
}
fn command(said: &str) -> Option<Wrapped> {
let name = unwrap_tag(said, "command-name")?.trim();
if name.is_empty() {
return None;
}
if controls_the_harness(name) {
return Some(Wrapped::Nothing);
}
let args = unwrap_tag(said, "command-args").unwrap_or_default().trim();
let printed = unwrap_tag(said, "local-command-stdout")
.unwrap_or_default()
.trim();
Some(Wrapped::Note(
Gloss::new(GlossKind::Command)
.gist(name)
.maybe_note((!args.is_empty()).then_some(args))
.maybe_plain((!printed.is_empty()).then_some(printed)),
))
}
fn told(kind: GlossKind, said: &str) -> Gloss {
const AT_A_GLANCE: usize = 90;
let opening = said.lines().next().unwrap_or(said).trim();
if said.lines().count() == 1 && said.chars().count() <= AT_A_GLANCE {
return Gloss::new(kind).gist(opening);
}
Gloss::new(kind).gist(opening).prose(said)
}
fn subject(attachment: &Value) -> Option<&str> {
["displayPath", "path", "filename"]
.iter()
.find_map(|field| text(attachment, field))
}
fn paragraphs(content: &Value) -> Option<String> {
let joined = content
.as_array()?
.iter()
.filter_map(Value::as_str)
.collect::<Vec<&str>>()
.join("\n\n");
(!joined.trim().is_empty()).then_some(joined)
}
fn unwrap_tag<'a>(text: &'a str, tag: &str) -> Option<&'a str> {
let opening = format!("<{tag}>");
let closing = format!("</{tag}>");
let from = text.find(&opening)? + opening.len();
let to = text[from..].find(&closing)? + from;
Some(&text[from..to])
}
fn strip_tag<'a>(text: &'a str, tag: &str) -> Cow<'a, str> {
let closing = format!("</{tag}>");
let Some(from) = text.find(&format!("<{tag}>")) else {
return Cow::Borrowed(text);
};
let Some(to) = text[from..]
.find(&closing)
.map(|at| at + from + closing.len())
else {
return Cow::Borrowed(text);
};
Cow::Owned(format!("{}{}", &text[..from], &text[to..]))
}
fn text<'a>(value: &'a Value, field: &str) -> Option<&'a str> {
value.get(field)?.as_str()
}
fn flag(value: &Value, field: &str) -> bool {
value.get(field).and_then(Value::as_bool).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn noted(said: &str) -> Option<Gloss> {
match command(said) {
Some(Wrapped::Note(gloss)) => Some(gloss),
_ => None,
}
}
#[test]
fn a_slash_command_is_read_out_of_its_wrapper() {
let gloss = noted(
"<command-message>debug-gha</command-message>\n\
<command-name>/debug-gha</command-name>",
)
.expect("the wrapper names a command");
assert_eq!(gloss.kind, GlossKind::Command);
assert_eq!(gloss.gist.as_deref(), Some("/debug-gha"));
assert!(gloss.notes.is_empty());
assert!(gloss.body.is_empty());
}
#[test]
fn a_command_carries_its_arguments_and_what_it_printed() {
let gloss = noted(
"<command-name>/loop</command-name>\n\
<command-message>loop</command-message>\n\
<command-args>5m /babysit-prs</command-args>\n\
<local-command-stdout>scheduled</local-command-stdout>",
)
.expect("the wrapper names a command");
assert_eq!(gloss.gist.as_deref(), Some("/loop"));
assert_eq!(gloss.notes, ["5m /babysit-prs"]);
assert_eq!(gloss.body, [Body::Plain("scheduled".into())]);
}
#[test]
fn a_command_that_only_controls_the_harness_is_not_set() {
assert_eq!(
command(
"<command-name>/copy</command-name>\n\
<local-command-stdout>Copied to clipboard (464 characters, 7 lines)\
</local-command-stdout>"
),
Some(Wrapped::Nothing)
);
assert_eq!(
command("<command-name>/config</command-name>"),
Some(Wrapped::Nothing)
);
}
#[test]
fn a_command_that_changes_the_conversation_is_still_set() {
let gloss = noted("<command-name>/compact</command-name>")
.expect("a command that changes the conversation is a gloss");
assert_eq!(gloss.gist.as_deref(), Some("/compact"));
}
#[test]
fn a_command_that_printed_nothing_has_no_fold() {
let gloss = noted(
"<command-name>/clear</command-name>\n\
<command-args></command-args>\n\
<local-command-stdout></local-command-stdout>",
)
.expect("the wrapper names a command");
assert!(gloss.notes.is_empty());
assert!(gloss.body.is_empty());
}
#[test]
fn the_caveat_wrapping_a_command_is_not_set() {
let said = "<local-command-caveat>Caveat: The messages below were generated \
by the user while running local commands.</local-command-caveat>\n\
<command-name>/review</command-name>";
let Some(Wrapped::Note(gloss)) = wrapped(said, true) else {
panic!("the command should survive the caveat");
};
assert_eq!(gloss.kind, GlossKind::Command);
assert_eq!(gloss.gist.as_deref(), Some("/review"));
}
#[test]
fn a_caveat_with_nothing_left_under_it_is_dropped_rather_than_left_as_a_turn() {
assert_eq!(
wrapped(
"<local-command-caveat>Caveat: do not respond.</local-command-caveat>",
true
),
Some(Wrapped::Nothing)
);
}
#[test]
fn what_the_user_actually_typed_is_left_in_the_conversation() {
assert_eq!(wrapped("Does the formatter run on nightly?", false), None);
}
#[test]
fn a_skill_is_named_by_its_directory_and_holds_its_instructions() {
let gloss = meta(
"Base directory for this skill: /home/jtk/.claude/skills/debug-gha\n\n\
# Debug GitHub Actions Runs\n\nThe objective is to **fix** the failing run.",
)
.expect("a skill body is a gloss");
assert_eq!(gloss.kind, GlossKind::Skill);
assert_eq!(gloss.gist.as_deref(), Some("debug-gha"));
assert_eq!(
gloss.body,
[Body::Prose(
"# Debug GitHub Actions Runs\n\nThe objective is to **fix** the failing run."
.into()
)]
);
}
#[test]
fn a_short_harness_note_is_stated_on_the_line_with_no_fold() {
let gloss = meta("Continue from where you left off").expect("a note is a gloss");
assert_eq!(gloss.kind, GlossKind::Note);
assert_eq!(
gloss.gist.as_deref(),
Some("Continue from where you left off")
);
assert!(gloss.body.is_empty());
}
#[test]
fn how_an_image_was_scaled_for_the_model_is_not_set() {
assert_eq!(
meta(
"[Image: original 1400x2006, displayed at 1396x2000. \
Multiply coordinates by 1.00 to map to original image.]"
),
None
);
}
#[test]
fn a_hook_sets_what_it_contributed_and_what_it_printed_to_stderr() {
let gloss = attachment(&json!({
"type": "hook_success",
"hookName": "SessionStart:clear",
"command": "claude-branch-journal",
"content": "",
"stdout": "",
"stderr": "No journal yet for branch 'wide-margins'\n",
"exitCode": 0,
}))
.expect("a hook that printed something is a gloss");
assert_eq!(gloss.kind, GlossKind::Hook);
assert_eq!(gloss.gist.as_deref(), Some("SessionStart:clear"));
assert_eq!(gloss.notes, ["claude-branch-journal"]);
assert_eq!(
gloss.body,
[Body::Plain(
"No journal yet for branch 'wide-margins'".into()
)]
);
}
#[test]
fn a_hooks_contribution_is_set_once_though_it_is_recorded_twice() {
let gloss = attachment(&json!({
"type": "hook_success",
"hookName": "SessionStart:clear",
"content": "Current git status:\n\n## wide-margins",
"stdout": "Current git status:\n\n## wide-margins\n",
"stderr": "",
"exitCode": 0,
}))
.expect("a hook that contributed context is a gloss");
assert_eq!(
gloss.body,
[Body::Plain("Current git status:\n\n## wide-margins".into())]
);
}
#[test]
fn a_failing_hook_says_what_it_exited_with() {
let gloss = attachment(&json!({
"type": "hook_success",
"hookName": "PreToolUse:Bash",
"command": "claude-read-check",
"stdout": "use the Read tool",
"exitCode": 2,
}))
.expect("a hook that printed something is a gloss");
assert_eq!(gloss.notes, ["claude-read-check", "exit 2"]);
}
#[test]
fn hook_output_in_the_control_protocol_is_left_to_the_notes_it_asks_for() {
assert_eq!(
attachment(&json!({
"type": "hook_success",
"hookName": "Stop",
"command": "claude-stop",
"content": "",
"stdout": "{\"systemMessage\": \"finishing pass\", \
\"hookSpecificOutput\": {\"additionalContext\": \"…\"}}",
"stderr": "",
"exitCode": 0,
})),
None
);
}
#[test]
fn context_a_hook_injected_is_joined_into_one_document() {
let gloss = attachment(&json!({
"type": "hook_additional_context",
"hookName": "Stop",
"content": ["first thing", "second thing"],
}))
.expect("injected context is a gloss");
assert_eq!(gloss.gist.as_deref(), Some("Stop"));
assert_eq!(gloss.notes, ["added context"]);
assert_eq!(
gloss.body,
[Body::Printed("first thing\n\nsecond thing".into())]
);
}
#[test]
fn a_rule_pulled_into_context_is_named_by_the_path_written_for_a_reader() {
let gloss = attachment(&json!({
"type": "nested_memory",
"path": "/home/jtk/dotfiles/claude/rules/rust.md",
"displayPath": "claude/rules/rust.md",
"content": {
"path": "/home/jtk/dotfiles/claude/rules/rust.md",
"type": "User",
"content": "# Rust Style Guide\n\nUse the latest stable edition.",
},
}))
.expect("a memory file is a gloss");
assert_eq!(gloss.kind, GlossKind::Rule);
assert_eq!(gloss.gist.as_deref(), Some("claude/rules/rust.md"));
assert_eq!(gloss.notes, ["user"]);
assert_eq!(
gloss.body,
[Body::Prose(
"# Rust Style Guide\n\nUse the latest stable edition.".into()
)]
);
}
#[test]
fn plan_mode_marks_its_boundaries_and_says_whether_a_plan_was_written() {
let entered = attachment(&json!({
"type": "plan_mode",
"reminderType": "full",
"isSubAgent": false,
"planFilePath": "/home/jtk/.claude/plans/wide-margins.md",
"planExists": false,
}))
.expect("entering plan mode is a gloss");
assert_eq!(entered.kind, GlossKind::Plan);
assert_eq!(entered.gist.as_deref(), Some("entered plan mode"));
assert!(entered.notes.is_empty());
let written = attachment(&json!({
"type": "plan_mode_exit",
"planFilePath": "/home/jtk/.claude/plans/wide-margins.md",
"planExists": true,
}))
.expect("leaving plan mode is a gloss");
assert_eq!(
written.gist.as_deref(),
Some("/home/jtk/.claude/plans/wide-margins.md")
);
assert_eq!(written.notes, ["left, plan written"]);
let unwritten = attachment(&json!({
"type": "plan_mode_exit",
"planFilePath": "/home/jtk/.claude/plans/wide-margins.md",
"planExists": false,
}))
.expect("leaving plan mode is a gloss");
assert_eq!(unwritten.notes, ["left, no plan written"]);
}
#[test]
fn a_plan_boundary_with_no_file_is_named_by_what_happened() {
let gloss = attachment(&json!({ "type": "plan_mode_exit", "planExists": false }))
.expect("leaving plan mode is a gloss even with no file named");
assert_eq!(gloss.gist.as_deref(), Some("left, no plan written"));
assert!(gloss.notes.is_empty());
}
#[test]
fn a_reminder_that_plan_mode_is_still_on_is_not_a_boundary() {
assert_eq!(
attachment(&json!({
"type": "plan_mode",
"reminderType": "sparse",
"planFilePath": "/home/jtk/.claude/plans/wide-margins.md",
"planExists": true,
})),
None
);
}
#[test]
fn a_file_edited_outside_the_session_is_set() {
let gloss = attachment(&json!({
"type": "edited_text_file",
"filename": "/home/jtk/projects/scriptorium/src/gloss.rs",
"snippet": "9\tfn setting() {}",
}))
.expect("an external edit is a gloss");
assert_eq!(gloss.kind, GlossKind::Note);
assert_eq!(gloss.notes, ["edited outside the session"]);
assert_eq!(gloss.body, [Body::Plain("9\tfn setting() {}".into())]);
}
#[test]
fn scaffolding_the_reader_cannot_act_on_is_not_set() {
for kind in [
"task_reminder",
"skill_listing",
"deferred_tools_delta",
"agent_listing_delta",
"command_permissions",
"date_change",
] {
assert_eq!(
attachment(&json!({ "type": kind, "content": "…" })),
None,
"{kind} should stay unset"
);
}
}
#[test]
fn a_local_commands_output_is_set_and_an_empty_one_is_not() {
let gloss = system(&json!({
"subtype": "local_command",
"content": "<local-command-stdout>Copied to clipboard</local-command-stdout>",
}))
.expect("a command that printed something is a gloss");
assert_eq!(gloss.kind, GlossKind::Command);
assert_eq!(gloss.body, [Body::Plain("Copied to clipboard".into())]);
assert_eq!(
system(&json!({
"subtype": "local_command",
"content": "<local-command-stdout></local-command-stdout>",
})),
None
);
}
#[test]
fn a_model_swapped_under_the_conversation_is_set() {
let gloss = system(&json!({
"subtype": "model_refusal_fallback",
"originalModel": "claude-fable-5",
"fallbackModel": "claude-opus-4-8",
"content": "The safeguards flagged this message.",
}))
.expect("a fallback is a gloss");
assert_eq!(gloss.gist.as_deref(), Some("model switched"));
assert_eq!(gloss.notes, ["claude-fable-5 → claude-opus-4-8"]);
}
#[test]
fn a_systems_own_bookkeeping_is_not_set() {
for subtype in ["turn_duration", "stop_hook_summary"] {
assert_eq!(
system(&json!({ "subtype": subtype, "content": "…" })),
None,
"{subtype} should stay unset"
);
}
}
}