Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/py-json.mjs
T
Vito Sansevero 564de9e3f1 feat(inspect): local candidate selection matching the inspector
The browser port of the inspection resolver now answers every record set
the server does, so no path is left to the server any more:

- several sampler candidates, with the identical-branch rule (#270): a
  compare grid whose branches all resolve to one pair (text and recipe)
  is resolved, any other mix is ambiguous;
- stages whose outputs reach only previews are dropped first;
- detailer stages are judged with the samplers in their image ancestry
  (multistage-differs, cycles, depth and the per-stage visit budgets),
  and a sampler that only feeds detailers is not a candidate of its own;
- A1111 `parameters`, with Python's multiline anchors, `\s` and Unicode
  `\d`, the bare footer and Template trailers, counted in code points.

The parity corpus gains the selection cases and a seeded fuzzer, and
every inspection vector now gives the server's prompts, resolution and
facts locally. 16 of 17 mutations are caught; the uncaught one (not
flattening an ambiguous upstream stage) cannot change any answer.
2026-10-22 17:43:33 -07:00

240 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Python's value model for the local inspection resolver (#249 local Inspect, PR L3): the server's resolver
// (utils/prompt_pair_resolver.py) runs on `json.loads` output, so the browser port reads the same text the same way.
// Objects are `Map`s (insertion order, a repeated key keeps its first position and its last value, as a dict does),
// integers are numbers (or BigInt past 2^53 - 1), floats are `PyFloat`, None is null. Strings are measured, sliced and
// stripped as Python's `str` is: by code point, with `str.isspace()` whitespace. No DOM.
/** A Python float: distinct from an int (`1.0` is not a link slot, `type(x) is int` is false). */
export class PyFloat {
constructor(value) {
this.value = value;
Object.freeze(this);
}
}
/** `ValueError` from `json.loads` (malformed text, or an integer past `sys.int_info.str_digits_check_threshold`). */
export class PyValueError extends Error {
constructor(message) {
super(message);
this.name = 'PyValueError';
}
}
// CPython's default int max str digits (PEP 651 / gh-95778).
const INT_MAX_STR_DIGITS = 4300;
const MAX_SAFE_DIGITS = '9007199254740991';
const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?/y;
const STRING_STOP = /["\\\u0000-\u001f]/g;
const HEX4 = /^[0-9a-fA-F]{4}$/;
const ESCAPES = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
// The C scanner's constants, in its order of checks.
const CONSTANTS = [['null', null], ['true', true], ['false', false], ['NaN', new PyFloat(NaN)],
['Infinity', new PyFloat(Infinity)], ['-Infinity', new PyFloat(-Infinity)]];
export const isDict = value => value instanceof Map;
export const isList = value => Array.isArray(value);
/** `isinstance(value, int) and not isinstance(value, bool)`. */
export const isInt = value => (typeof value === 'number' && Number.isInteger(value)) || typeof value === 'bigint';
function numberValue(match) {
const [literal, fraction, exponent] = match;
if (fraction || exponent) return new PyFloat(Number(literal));
const digits = literal[0] === '-' ? literal.slice(1) : literal;
if (digits.length > INT_MAX_STR_DIGITS) throw new PyValueError('Exceeds the limit for integer string conversion');
if (digits.length > MAX_SAFE_DIGITS.length || (digits.length === MAX_SAFE_DIGITS.length && digits > MAX_SAFE_DIGITS)) return BigInt(literal);
// `-0` is the int 0.
return Number(literal) || 0;
}
/** `json.loads(text)` with Python's defaults (C scanner): NaN/Infinity accepted, a repeated key's last value wins. */
export function pyJsonLoads(text) {
if (typeof text !== 'string') throw new TypeError('JSON text must be a string');
if (text.startsWith('')) throw new PyValueError('Unexpected UTF-8 BOM (decode using utf-8-sig)');
let pos = 0;
const fail = message => new PyValueError(`${message}: char ${pos}`);
const skip = () => {
while (pos < text.length) {
const c = text.charCodeAt(pos);
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break;
pos++;
}
};
function string() {
pos++;
let out = '';
for (;;) {
STRING_STOP.lastIndex = pos;
const stop = STRING_STOP.exec(text);
if (!stop) throw fail('Unterminated string starting at');
out += text.slice(pos, stop.index);
pos = stop.index + 1;
if (stop[0] === '"') return out;
if (stop[0] !== '\\') throw fail('Invalid control character at');
const escape = text[pos];
if (escape === 'u') {
const hex = text.slice(pos + 1, pos + 5);
if (!HEX4.test(hex)) throw fail('Invalid \\uXXXX escape');
pos += 5;
const code = parseInt(hex, 16);
if (code >= 0xd800 && code <= 0xdbff && text[pos] === '\\' && text[pos + 1] === 'u') {
const low = text.slice(pos + 2, pos + 6);
if (!HEX4.test(low)) throw fail('Invalid \\uXXXX escape');
const lowCode = parseInt(low, 16);
if (lowCode >= 0xdc00 && lowCode <= 0xdfff) {
out += String.fromCharCode(code, lowCode);
pos += 6;
continue;
}
}
out += String.fromCharCode(code);
} else if (Object.hasOwn(ESCAPES, escape)) {
out += ESCAPES[escape];
pos++;
} else throw fail('Invalid \\escape');
}
}
function key() {
if (text[pos] !== '"') throw fail('Expecting property name enclosed in double quotes');
const name = string();
skip();
if (text[pos] !== ':') throw fail("Expecting ':' delimiter");
pos++;
skip();
return name;
}
function scalar() {
if (text[pos] === '"') return string();
for (const [literal, value] of CONSTANTS) {
if (text.startsWith(literal, pos)) {
pos += literal.length;
return value;
}
}
NUMBER.lastIndex = pos;
const match = NUMBER.exec(text);
if (!match) throw fail('Expecting value');
pos += match[0].length;
return numberValue(match);
}
skip();
const stack = [];
let result;
for (;;) {
let value;
if (text[pos] === '{') {
pos++; skip();
const frame = { value: new Map(), object: true, key: null };
if (text[pos] === '}') pos++;
else { frame.key = key(); stack.push(frame); continue; }
value = frame.value;
} else if (text[pos] === '[') {
pos++; skip();
const frame = { value: [], object: false };
if (text[pos] === ']') pos++;
else { stack.push(frame); continue; }
value = frame.value;
} else value = scalar();
for (;;) {
if (!stack.length) { result = value; break; }
const frame = stack[stack.length - 1];
if (frame.object) frame.value.set(frame.key, value);
else frame.value.push(value);
skip();
if (text[pos] === ',') {
pos++; skip();
if (frame.object) frame.key = key();
break;
}
if (text[pos] !== (frame.object ? '}' : ']')) throw fail("Expecting ',' delimiter");
pos++;
stack.pop();
value = frame.value;
}
if (!stack.length) break;
}
skip();
if (pos !== text.length) throw fail('Extra data');
return result;
}
/** `len(text)`: code points (a lone surrogate counts once). */
export function pyLen(text) {
let pairs = 0;
for (let index = 0; index < text.length - 1; index++) {
const code = text.charCodeAt(index);
if (code >= 0xd800 && code <= 0xdbff) {
const next = text.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) { pairs++; index++; }
}
}
return text.length - pairs;
}
/** `text[:count]` by code points. */
export function pySlice(text, count) {
if (text.length <= count) return text;
let end = 0;
for (let taken = 0; taken < count && end < text.length; taken++) {
const code = text.charCodeAt(end);
const next = text.charCodeAt(end + 1);
end += code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff ? 2 : 1;
}
return text.slice(0, end);
}
// `str.isspace()`: Unicode Zs plus the bidi classes B, S and WS (not U+FEFF, unlike `trim()`).
export const PY_SPACE = '\\t\\n\\x0b\\x0c\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000';
const LEADING = new RegExp(`^[${PY_SPACE}]+`), TRAILING = new RegExp(`[${PY_SPACE}]+$`);
/** `text.strip()`. */
export const pyStrip = text => text.replace(LEADING, '').replace(TRAILING, '');
/** `bool(value)`. */
export function pyTruthy(value) {
if (value === null || value === undefined || value === false) return false;
if (value === true) return true;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'bigint') return value !== 0n;
if (value instanceof PyFloat) return value.value !== 0;
if (typeof value === 'string' || isList(value)) return value.length > 0;
if (isDict(value)) return value.size > 0;
return true;
}
/** `value == 1` (True and 1.0 equal it too). */
export const pyEqualsOne = value => value === 1 || value === 1n || value === true || (value instanceof PyFloat && value.value === 1);
/** `repr(float)`: the shortest round-trip digits, fixed for exponents -4..15, else `d.ddde+XX`. */
export function pyFloatRepr(value) {
if (Number.isNaN(value)) return 'nan';
if (!Number.isFinite(value)) return value > 0 ? 'inf' : '-inf';
if (value === 0) return Object.is(value, -0) ? '-0.0' : '0.0';
const [mantissa, exponentText] = value.toExponential().split('e');
const exponent = Number(exponentText);
const sign = mantissa.startsWith('-') ? '-' : '';
const digits = mantissa.replace(/^-/, '').replace('.', '');
if (exponent < -4 || exponent >= 16) {
const rest = digits.slice(1);
return `${sign}${digits[0]}${rest ? `.${rest}` : ''}e${exponent < 0 ? '-' : '+'}${String(Math.abs(exponent)).padStart(2, '0')}`;
}
if (exponent < 0) return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`;
const whole = digits.slice(0, exponent + 1).padEnd(exponent + 1, '0'), fraction = digits.slice(exponent + 1);
return `${sign}${whole}.${fraction || '0'}`;
}
/**
* `str(value)` where the resolver compares the result with node class names (`str(node.get("class_type", ""))`).
* A container's text never equals a class name, so it is a marker only.
*/
export function pyStr(value) {
if (typeof value === 'string') return value;
if (value === null || value === undefined) return 'None';
if (value === true) return 'True';
if (value === false) return 'False';
if (typeof value === 'number' || typeof value === 'bigint') return String(value);
if (value instanceof PyFloat) return pyFloatRepr(value.value);
return '\u0000container';
}
/** `mapping.get(key, fallback)`. */
export const pyGet = (mapping, key, fallback = null) => mapping.has(key) ? mapping.get(key) : fallback;