(subJson: Record<string, MathJSON>)
| 1062 | return { |
| 1063 | ok: false, |
| 1064 | reason: 'box-error', |
| 1065 | detail: String((err as Error)?.message) |
| 1066 | .replace(/\s+/g, ' ') |
| 1067 | .slice(0, 160), |
| 1068 | }; |
| 1069 | } |
| 1070 | } |
| 1071 | |
| 1072 | const fireTest = (subJson: Record<string, MathJSON>): { ok: boolean; detail?: string } => { |
| 1073 | try { |
| 1074 | const inst = ce.expr(substituteWildcards(matchW, subJson) as never); |
| 1075 | if (!inst.isValid) return { ok: false, detail: 'invalid instantiated match' }; |
| 1076 | const expected = ce.expr(substituteWildcards(replaceW, subJson) as never); |
| 1077 | if (!expected.isValid) |
| 1078 | return { ok: false, detail: 'invalid instantiated replace' }; |
| 1079 | const result = inst.replace(ruleSet); |
| 1080 | if (result === null) return { ok: false, detail: 'rule did not fire' }; |
| 1081 | // Structural identity, with two fallbacks. Syntactic: `isSame` |
| 1082 | // compares symbol BINDING identity, and rule substitution can |
| 1083 | // transplant a binder-bound occurrence outside its binder (the |
| 1084 | // derivative-of-lambda rules capture the lambda's own `_z`), so the |
| 1085 | // fired result and the expectation can be name-identical yet carry |
| 1086 | // different bindings — both sides are built from the same seed |
| 1087 | // symbols, so name equality is the honest shape check here. |
| 1088 | // Provable-equality: canonical form is not perfectly confluent (e.g. |
| 1089 | // `pi * sqrt(2) / 2` vs `sqrt(2)/2 * pi`), and the fired result and |
| 1090 | // the independently boxed expectation may settle in different but |
| 1091 | // equal canonical shapes. |
| 1092 | if ( |
| 1093 | !result.isSame(expected) && |
| 1094 | !sameSyntactic(result, expected) && |
| 1095 | result.isEqual(expected) !== true |
| 1096 | ) |
| 1097 | return { |
no test coverage detected