fix(code-runtime-python): make checkDoneValue over-budget precedence order-independent

checkDoneValue returned non-lossless the instant it hit a non-finite/negative-
zero number, before finishing the budget metering. A value that is BOTH over-
budget and non-lossless then classified by member order: `["<huge>", 1e400]`
gave non-lossless while `[1e400, "<huge>"]` gave over-budget — the same value,
two verdicts — which would drive the consumer to emit invalid-output vs
output-limit non-deterministically, contradicting the JSDoc promise that an
over-budget value is rejected as over-budget regardless. Record the number
violation in a flag and let metering finish; return non-lossless only once the
whole value is confirmed within budget. Add a regression test asserting both
member orders classify as over-budget.
This commit is contained in:
Chinesezjc
2026-08-07 13:27:54 +08:00
parent 8cf253a470
commit 146a9d9f61
2 changed files with 25 additions and 2 deletions
@@ -217,12 +217,19 @@ function scalarJson(current: unknown): string {
*/
export function checkDoneValue(value: unknown, maxBytes: number): { ok: true; bytes: number } | { ok: false; reason: 'over-budget' | 'non-lossless' } {
let bytes = 0
// A non-lossless number is recorded, not returned on sight: over-budget must
// win regardless of where in the value each violation sits, so the whole
// metering finishes first. Otherwise `["<huge>", 1e400]` and `[1e400,
// "<huge>"]` — the same over-budget value in two member orders — would
// classify differently (non-lossless vs over-budget), and the JSDoc promises
// an over-budget value is rejected as over-budget regardless.
let nonLossless = false
const stack: unknown[] = [value]
while (stack.length > 0) {
const current = stack.pop()
if (typeof current === 'number') {
if (!Number.isFinite(current) || Object.is(current, -0)) return { ok: false, reason: 'non-lossless' }
bytes += Buffer.byteLength(scalarJson(current), 'utf8')
if (!Number.isFinite(current) || Object.is(current, -0)) nonLossless = true
else bytes += Buffer.byteLength(scalarJson(current), 'utf8')
} else if (typeof current === 'string') {
// Lower-bound BEFORE materializing the escaped form: every UTF-16 code
// unit is at least one UTF-8 byte plus the two quotes, so a huge or
@@ -262,6 +269,8 @@ export function checkDoneValue(value: unknown, maxBytes: number): { ok: true; by
}
if (bytes > maxBytes) return { ok: false, reason: 'over-budget' }
}
// The whole value fit the budget; a recorded number violation is the verdict.
if (nonLossless) return { ok: false, reason: 'non-lossless' }
return { ok: true, bytes }
}