From 46e24993a5b5bcfbbbfd70f20bd76f708f947c04 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya Date: Tue, 18 Aug 2026 16:56:42 -0700 Subject: [PATCH 1/4] fix(schema): count the blast radius in the narrowing confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The narrowing confirmation dialog asked to confirm without saying what confirming costs. It now states how many classes narrow and that zero annotations use what is removed — a measured number, since blockers is always empty on a preview that reaches this dialog. Drops the 'existing annotations are not touched' line, which promised something about post-publish annotation state the kernel has not decided. --- frontend/ui-core/src/screens/SchemaEditor.tsx | 30 +++++++++++-- frontend/ui-core/src/screens/screens.test.tsx | 43 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/frontend/ui-core/src/screens/SchemaEditor.tsx b/frontend/ui-core/src/screens/SchemaEditor.tsx index 7580b3c6..6cb89983 100644 --- a/frontend/ui-core/src/screens/SchemaEditor.tsx +++ b/frontend/ui-core/src/screens/SchemaEditor.tsx @@ -1250,7 +1250,26 @@ function describeDestructiveClasses(classes: readonly string[]): string { return `${named.slice(0, -1).join(", ")}, and ${named.at(-1) ?? "another class"}`; } -/** Retryable: a publishable preview says the change narrows the contract. */ +/** + * Retryable: a publishable preview says the change narrows the contract. + * + * The counts are the point. A confirmation that asks "are you sure?" without + * saying what yes costs is a speed bump, and the two numbers worth saying are + * both already measured: how many classes narrow, and how many annotations are + * at risk. The second is always zero here — a preview carrying blockers is + * refused outright and never reaches this dialog — and a measured zero is the + * reason the publish is offered at all, so it is stated rather than implied. + * + * It says nothing about what becomes of annotations *after* the publish. An + * open batch pinned to the outgoing version can still write the removed class, + * and whether that is tolerated or forbidden is not something this dialog is + * entitled to answer. + * + * A shape removed from a class and the class itself removed are indistinguishable + * here: the wire's change record carries the class name but not the geometry, so + * the copy counts classes, which is true of both, rather than guessing which + * happened. + */ function DestructiveDialog({ preview, pending, @@ -1268,11 +1287,14 @@ function DestructiveDialog({ This narrows the schema - This change narrows the schema for {describeDestructiveClasses(destructiveClasses)}. + {formatCount(destructiveClasses.length)}{" "} + {destructiveClasses.length === 1 ? "class narrows" : "classes narrow"}:{" "} + {describeDestructiveClasses(destructiveClasses)}. - Existing annotations are not touched. Saving anyway publishes the new version and leaves - earlier ones exactly as they are — a version is immutable. + No annotations use what this removes — that is why it can be published at all. Publishing + creates a new version; the versions before it keep everything they already declared, + because a version is immutable.