test: the Resize channel of the junction limits, driven by a real pointer (#329 AC7a)

Measured what Resize itself already forbids: a room cannot be squeezed below
30 cm (two 15 cm walls), so П3 and П5 are unreachable through shrinking and
the gate merely fails closed there. П4 IS reachable on a fine grid, so the
smoke drags a real handle on a 2 cm grid: two rooms 10 cm apart, a 6 cm pull
would leave 4 cm between foreign nodes, the wall stops at 6 cm and exactly one
toast names the 5 cm rule.

Spec revision 6 records both the measurement and the two corrections it forces
on AC7a: a dimmed handle cannot express a per-step limit, and the plan is NOT
byte-unchanged — the allowed part of the gesture is a legitimate edit.

Issue: #329
User-Visible: no
This commit is contained in:
Codex
2026-08-27 23:31:42 +03:00
parent f514fb27fe
commit 2b1f03be2d
2 changed files with 108 additions and 5 deletions
+85 -1
View File
@@ -119,5 +119,89 @@ const out = await page.evaluate(async () => {
return result;
});
checkAll(out);
// AC7a (канал Resize): стена упирается в последнюю ДОПУСТИМУЮ позицию, а не
// проезжает нарушение. Сетка 2 см: две комнаты в 10 см друг от друга, тяга
// на 6 см вправо оставила бы 4 см между чужими узлами (П4 — минимум 5 см).
// Жест настоящий: pointer по ручке, без вызовов внутренних методов Resize.
const settle = () => page.evaluate(() => new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve))));
await page.evaluate(async () => {
const card = window.__card;
window.__toasts = [];
card._showToast = (text) => { window.__toasts.push(String(text)); };
const cm = (value) => (value / 2) * (1000 / 240);
card._serverCfg = { spaces: [{
id: 'limits', title: 'Limits', cell_cm: 2, view_box: [0, 0, 1, 1],
rooms: [], openings: [], room_drafts: [], partitions: [], wall_columns: [],
}], markers: [], settings: {} };
card._space = 'limits'; card._layout = {};
card._cfgEpoch++; card._modelCache = null; card._frame = null;
card._setMode('plan'); card._tool = 'draw'; card._drawWallField = '15';
card.requestUpdate(); await card.updateComplete;
const draw = (points, name) => {
card._path = [...points, points[0]];
card._nameSel = name; card._areaSel = '';
card._commitRoom();
card._path = [];
};
draw([[200, 700], [200 + cm(200), 700], [200 + cm(200), 700 - cm(200)],
[200, 700 - cm(200)]], 'A');
draw([[200 + cm(210), 700], [200 + cm(400), 700],
[200 + cm(400), 700 - cm(200)], [200 + cm(210), 700 - cm(200)]], 'B');
card._tool = 'resize';
card.requestUpdate(); await card.updateComplete;
window.__toasts.length = 0;
});
await settle();
await page.waitForTimeout(700);
await page.evaluate(() => { window.__toasts.length = 0; });
// Просвет между правой гранью A и левой гранью B, в сантиметрах.
const gapCm = () => page.evaluate(() => {
const rooms = window.__card._curSpaceCfg.rooms;
const a = Math.max(...rooms.find((room) => room.name === 'A').poly.map((p) => p[0]));
const b = Math.min(...rooms.find((room) => room.name === 'B').poly.map((p) => p[0]));
return Math.round(((b - a) * 1000) / (1000 / 240) * 2);
});
const handle = await page.evaluate(() => {
const card = window.__card;
const handles = [...card.renderRoot.querySelectorAll('.rszhandle[aria-disabled="false"]')];
const rooms = card._curSpaceCfg.rooms;
const aRight = Math.max(...rooms.find((room) => room.name === 'A').poly.map((p) => p[0])) * 1000;
const found = handles.find((entry) => Math.abs(Number(entry.getAttribute('cx')) - aRight) < 1.5);
if (!found) return null;
const svg = found.ownerSVGElement;
const map = (x, y) => {
const point = svg.createSVGPoint();
point.x = x; point.y = y;
const mapped = point.matrixTransform(found.getScreenCTM());
return [mapped.x, mapped.y];
};
const cx = Number(found.getAttribute('cx'));
const cy = Number(found.getAttribute('cy'));
return { start: map(cx, cy), end: map(cx + (6 / 2) * (1000 / 240), cy) };
});
const resize = { resizeHandleFound: !!handle, resizeGapBefore: await gapCm() };
if (handle) {
await page.mouse.move(...handle.start);
await page.mouse.down();
await page.mouse.move(...handle.end, { steps: 10 });
await settle();
await page.mouse.up();
await settle();
await page.waitForTimeout(700);
const toasts = await page.evaluate(() => window.__toasts.slice());
// 6 см дало бы 4 см — отказ; стена стоит на 6 см, последней допустимой.
resize.resizeStoppedAtLastAllowed = await gapCm();
resize.resizeRefusalNamesRule = toasts.some((text) => text.includes('5'));
resize.resizeRefusalOnce = toasts.length;
}
checkAll({ ...out, ...resize }, {
resizeGapBefore: 10,
resizeStoppedAtLastAllowed: 6,
resizeRefusalOnce: 1,
});
await finish(browser);
+23 -4
View File
@@ -162,10 +162,29 @@ margin): правила П1–П5 применяются к элементам,
ровно 3 различные вершины без ступенек и микро-сегментов; golden-сцена +
юниты на контурах и на кольце.
- **AC7a (Resize, канал §2).** Resize, приводящий к нарушению любого из
П1–П5, упирается в последнюю допустимую позицию; при отсутствии
безопасного шага ручка приглушена, и её объяснение (hover/focus/нажатие)
называет нарушенное правило. План байт-неизменен; тост не показывается.
Смок проверяет и байт-неизменность, и текст объяснения ручки.
П1–П5, упирается в последнюю ДОПУСТИМУЮ позицию: нарушающий шаг не
превьюится и не коммитится, а отказ ровно один раз за жест называет
нарушенное правило тем же каналом, которым Resize уже объясняет
отклонённый предпросмотр (`resize.preview_failed` → `resize.limit_stopped`).
Смок ведёт настоящий pointer-жест по ручке: сетка 2 см, две комнаты в 10 см
друг от друга, тяга на 6 см (оставила бы 4 см между чужими узлами, П4)
останавливает стену на 6 см и показывает ровно один тост с «5 см».
> Ревизия 6, отступление от первоначальной формулировки («ручка приглушена,
> тост не показывается, план байт-неизменен»). Приглушение ручки считается
> статически в `_rszResolution`, до жеста, а нарушение стыка зависит от
> КОНКРЕТНОГО шага — статически его не разрешить, не перебирая все позиции.
> Молчаливый отказ при этом прямо противоречит уже действующему контракту
> #293/#295 («reject не молчит»), закреплённому мутантом
> `resize-preview-reject-silent`. И байт-неизменность плана неверна как
> общее требование: разрешённая часть жеста — это нормальная правка,
> которую пользователь и просил; неизменным план остаётся только когда
> допустимого шага нет вовсе.
>
> Замер собственных ограничителей Resize (2026-08-27): комнату не дают
> сузить ниже 30 см (две толщины по 15 см), поэтому П3 (20 см) и П5 через
> сужение комнаты недостижимы — гейт там страхует, а не работает. Реально
> достижимо П4 на мелкой сетке, этот случай и закреплён смоком.
- **AC7b («Толщина», канал §2).** Значение толщины, нарушающее П3/П5, не
применяется; показывается тост с названием правила (образец отказа нуля
#313). План байт-неизменен. Смок проверяет текст тоста.