fix(web): reject an underflowing literal, and route embedded JSON resources

Copilot's thirteenth pass, two comments, both real.

- `1e-400` has a nonzero mantissa and parses to `0`. The digit comparison skips
  it for being exponent-form and it is not negative zero, so nothing saw a
  document that says one number and sends none of it. Caught now by pairing
  `Number(literal) === 0` with a nonzero digit in the mantissa, which leaves a
  literal written as zero (`0e-400`) and a subnormal that still parses to
  something (`1e-320`) alone.

- The `resource` content-block branch rendered every embedded resource in a
  plain code block, ignoring `resource.mimeType`, so a prompt message carrying
  JSON did not inherit this migration even though the text-block branch did. An
  embedded *text* resource declaring JSON now goes through the same dispatch.

Narrowed to JSON, as the Network tab's bodies are: routing every declared type
through would also put an embedded `text/html` resource into the sandboxed
frame and a `text/csv` one into a table. The Resources screen already renders
resource contents that way, so doing it here too is defensible — but it is a
change to what a tool result *is*, and belongs to whoever decides that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-27 06:26:23 -04:00
commit 684eb9c7bc
2 changed files with 96 additions and 7 deletions

View file

@ -78,9 +78,26 @@ describe("findUnsendableNumberLiteral", () => {
},
);
// A nonzero literal that parses to zero anyway: the digit comparison skips it
// for being exponent-form, and it is not negative zero, so nothing else here
// would see a document that says one number and sends none of it.
it.each(["1e-400", "-1e-400", "0.00001e-400", "9e-999"])(
"rejects a literal that underflows to zero: %s",
(literal) => {
expect(findUnsendableNumberLiteral(`{"n":${literal}}`)).toBe(literal);
},
);
// Subnormal but representable — a real value, not an underflow to nothing.
it("accepts a subnormal that still parses to something", () => {
expect(findUnsendableNumberLiteral('{"n":1e-320}')).toBeNull();
});
it("accepts a positive zero", () => {
expect(findUnsendableNumberLiteral('{"n":0}')).toBeNull();
expect(findUnsendableNumberLiteral('{"n":0.0}')).toBeNull();
// Written as zero, so it is zero — not an underflow.
expect(findUnsendableNumberLiteral('{"n":0e-400}')).toBeNull();
});
it("says nothing about text that is not JSON", () => {

View file

@ -41,9 +41,10 @@ export function isJsonObject(value: unknown): value is StrictJsonObject {
* Only integer literals written out in full are checked for lost digits. An
* exponent or a fraction is a double by nature and is sent as the double it
* parsed to, so nothing is lost between typing and sending — and `{"n":1e308}`
* stays acceptable, which `parseJsonObjectDraft`'s own tests pin. Negative zero
* is the exception, and is checked in every spelling: there the loss happens on
* the way *out*, not on the way in.
* stays acceptable, which `parseJsonObjectDraft`'s own tests pin. The two
* exceptions are checked in every spelling: negative zero, where the loss
* happens on the way *out*, and underflow, where a nonzero literal parses to
* zero on the way in.
*/
export function findUnsendableNumberLiteral(text: string): string | null {
for (const literal of jsonNumberLiterals(text)) {
@ -51,6 +52,13 @@ export function findUnsendableNumberLiteral(text: string): string | null {
// exactly, so the digit comparison below has nothing to say about it, but
// `JSON.stringify` writes it as `0`: the sign is lost on the way out.
if (Object.is(Number(literal), -0)) return literal;
// Underflow: a literal with a nonzero mantissa that parses to zero anyway,
// `1e-400` being the shape of it. The digit comparison skips it for being
// exponent-form, and it is not negative zero, so nothing else here sees a
// document that says one number and sends none of it.
if (Number(literal) === 0 && /[1-9]/.test(mantissaOf(literal))) {
return literal;
}
if (!/^-?\d+$/.test(literal)) continue;
// BigInt on both sides, so the comparison is exact: `Number(literal)` is an
// integer-valued double, and converting it back shows what survived.
@ -59,6 +67,15 @@ export function findUnsendableNumberLiteral(text: string): string | null {
return null;
}
/**
* The part of a numeric literal before its exponent — `1` of `1e-400`, `-0.0`
* of `-0.0e5`. Whether it holds a nonzero digit is what separates a literal
* that underflowed to zero from one that was written as zero.
*/
function mantissaOf(literal: string): string {
return literal.split(/[eE]/)[0];
}
/** Matches one JSON number token, anchored where the scan currently stands. */
const JSON_NUMBER = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y;