refactor: missing-fact discipline in proof witness generation

- Remove defensive ?? 0 fallbacks in buildFailedPredicate that could
  silently report false geometry in proof witnesses
- Require all 4 rect fields present before building subjectRect/referenceRect
  (was only checking subjectLeft)
- Add explicit undefined guards for observed gap, measured values,
  delta, and aspect ratio — return undefined (no failedPredicate)
  when critical metrics are missing instead of fabricating 0 defaults
- Generic synthesizeGenericFailedPredicate checks value presence
  instead of defaulting metrics[keys] to 0
- Option defaults (minGap, maxGap, tolerance, min, max bounds)
  retain ?? 0/?? Infinity as correct neutral values for user parameters
This commit is contained in:
John Dvorak
2026-05-22 16:17:18 -07:00
parent ce04b2b3de
commit 3b7be0aaf0
+56 -35
View File
@@ -59,9 +59,20 @@ function buildFailedPredicate(
// --- Directional gap (leftOf / rightOf / above / below) ---
if (hasGap) {
const gap = metrics.observedGap ?? metrics.gap ?? 0;
const gap = metrics.observedGap ?? metrics.gap;
if (gap === undefined) return undefined;
const min = metrics.minGap ?? 0;
const max = metrics.maxGap ?? Infinity;
const hasSubjectRect =
metrics.subjectLeft !== undefined &&
metrics.subjectTop !== undefined &&
metrics.subjectRight !== undefined &&
metrics.subjectBottom !== undefined;
const hasRefRect =
metrics.refLeft !== undefined &&
metrics.refTop !== undefined &&
metrics.refRight !== undefined &&
metrics.refBottom !== undefined;
return {
...base,
op: gap < min ? '<' : '>',
@@ -70,22 +81,20 @@ function buildFailedPredicate(
measuredGap: gap,
expectedMinGap: Number.isFinite(min) ? min : undefined,
expectedMaxGap: Number.isFinite(max) ? max : undefined,
subjectRect:
metrics.subjectLeft !== undefined
subjectRect: hasSubjectRect
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
top: metrics.subjectTop,
right: metrics.subjectRight,
bottom: metrics.subjectBottom,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
referenceRect: hasRefRect
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
top: metrics.refTop,
right: metrics.refRight,
bottom: metrics.refBottom,
}
: undefined,
};
@@ -93,37 +102,41 @@ function buildFailedPredicate(
// --- Containment / inside (unique overflow metrics) ---
if (kind === 'inside') {
const overflowLeft = metrics.overflowLeft ?? 0;
const overflowTop = metrics.overflowTop ?? 0;
const overflowRight = metrics.overflowRight ?? 0;
const overflowBottom = metrics.overflowBottom ?? 0;
const hasSubjectRect =
metrics.subjectLeft !== undefined &&
metrics.subjectTop !== undefined &&
metrics.subjectRight !== undefined &&
metrics.subjectBottom !== undefined;
const hasRefRect =
metrics.refLeft !== undefined &&
metrics.refTop !== undefined &&
metrics.refRight !== undefined &&
metrics.refBottom !== undefined;
return {
...base,
op: 'not-contained',
left: 0,
right: 0,
overflowEdges: {
left: overflowLeft,
top: overflowTop,
right: overflowRight,
bottom: overflowBottom,
left: metrics.overflowLeft ?? 0,
top: metrics.overflowTop ?? 0,
right: metrics.overflowRight ?? 0,
bottom: metrics.overflowBottom ?? 0,
},
subjectRect:
metrics.subjectLeft !== undefined
subjectRect: hasSubjectRect
? {
left: metrics.subjectLeft,
top: metrics.subjectTop ?? 0,
right: metrics.subjectRight ?? 0,
bottom: metrics.subjectBottom ?? 0,
top: metrics.subjectTop,
right: metrics.subjectRight,
bottom: metrics.subjectBottom,
}
: undefined,
referenceRect:
metrics.refLeft !== undefined
referenceRect: hasRefRect
? {
left: metrics.refLeft,
top: metrics.refTop ?? 0,
right: metrics.refRight ?? 0,
bottom: metrics.refBottom ?? 0,
top: metrics.refTop,
right: metrics.refRight,
bottom: metrics.refBottom,
}
: undefined,
};
@@ -131,7 +144,8 @@ function buildFailedPredicate(
// --- Size threshold (atLeast / atMost / between) ---
if (isSize && kind !== 'aspectRatio') {
const observed = metrics.observed ?? metrics.value ?? 0;
const observed = metrics.observed ?? metrics.value;
if (observed === undefined) return undefined;
const min = metrics.min ?? -Infinity;
const max = metrics.max ?? Infinity;
return {
@@ -147,7 +161,8 @@ function buildFailedPredicate(
// --- Aspect ratio ---
if (kind === 'aspectRatio') {
const observed = metrics.observed ?? 0;
const observed = metrics.observed;
if (observed === undefined) return undefined;
const minRatio = metrics.minRatio ?? -Infinity;
const maxRatio = metrics.maxRatio ?? Infinity;
return {
@@ -163,7 +178,8 @@ function buildFailedPredicate(
// --- Alignment (alignedWith / centeredWithin) ---
if (hasAxis || kind === 'centeredWithin') {
const delta = metrics.delta ?? metrics.deltaX ?? metrics.deltaY ?? 0;
const delta = metrics.delta ?? metrics.deltaX ?? metrics.deltaY;
if (delta === undefined) return undefined;
const tolerance = metrics.tolerance ?? 0;
return {
...base,
@@ -188,16 +204,21 @@ function synthesizeGenericFailedPredicate(
): Proof['failedPredicate'] {
const keys = Object.keys(metrics);
if (keys.length >= 2) {
const left = metrics[keys[0]];
const right = metrics[keys[1]];
if (left === undefined || right === undefined) return undefined;
return {
op: '<',
left: metrics[keys[0]] ?? 0,
right: metrics[keys[1]] ?? 0,
left,
right,
};
}
if (keys.length === 1) {
const val = metrics[keys[0]];
if (val === undefined) return undefined;
return {
op: '<',
left: metrics[keys[0]] ?? 0,
left: val,
right: 0,
};
}