Skip to content

Commit d171bbd

Browse files
authored
Hotfix (#149)
1 parent 566a072 commit d171bbd

4 files changed

Lines changed: 51 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This document summarizes notable updates since February 2025, with commit dates from the repository history for context.【728428†L1-L48】
44

55
## 2026-03
6+
- **Bugfix:** Fixed `canonicalizeSpotTrace` corrupting SPOT traces containing `|` (OR) in state formulas (e.g. `(s & v) | (!s & !v)`). The canonicalizer now resolves OR operators before reordering literals, preventing garbled traces that caused wrong correct-answer labels on trace satisfaction questions.
67
- Fixed trace literal canonicalization to normalize both `!` and `¬` negation markers before lexicographic sorting, so variable ordering is stable regardless of glyph choice.
78
- Fixed instructor exercise preview so it renders the current draft question set (including unsaved editor changes) instead of only the previously saved database state.
89
- Added explicit save confirmation showing how many questions were persisted for instructor exercises, reducing ambiguity when validating multi-question saves.

src/exerciseprocessor.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,25 +204,41 @@ def nodeReprListsToSpotTrace(prefix_states, cycle_states) -> str:
204204
return prefix_string + ";" + cycle_string
205205

206206

207+
def _resolve_ors_in_state(state_str: str) -> str:
208+
"""Resolve any OR (|) operators in a SPOT state formula by choosing one branch.
209+
SPOT traces may contain states like '(s & v) | (!s & !v)' which represent
210+
multiple valid assignments. This picks one concrete assignment."""
211+
state_str = state_str.strip()
212+
if not state_str or state_str in ('1', '0'):
213+
return state_str
214+
if '|' not in state_str:
215+
return state_str
216+
try:
217+
return choosePathFromWord(state_str)
218+
except Exception:
219+
return state_str
220+
221+
207222
def canonicalizeSpotTrace(sr: str) -> str:
208223
"""
209224
Return a trace string with each state's literals in canonical order.
210225
This preserves the original prefix/cycle structure and only normalizes
211-
literal ordering within states.
226+
literal ordering within states. OR operators in SPOT state formulas are
227+
resolved first by choosing one concrete assignment.
212228
"""
213229
sr = sr.strip()
214230
if sr == "":
215231
return ""
216232

217233
prefix_split = sr.split('cycle', 1)
218234
prefix_parts = [x for x in prefix_split[0].strip().split(';') if x.strip() != ""]
219-
canonical_prefix = [NodeRepr._canonicalize_state(part) for part in prefix_parts]
235+
canonical_prefix = [NodeRepr._canonicalize_state(_resolve_ors_in_state(part)) for part in prefix_parts]
220236

221237
cycle_parts = []
222238
if len(prefix_split) > 1:
223239
cycled_content = getCycleContent(prefix_split[1])
224240
cycle_parts = [x for x in cycled_content.split(';') if x.strip() != ""]
225-
cycle_parts = [NodeRepr._canonicalize_state(part) for part in cycle_parts]
241+
cycle_parts = [NodeRepr._canonicalize_state(_resolve_ors_in_state(part)) for part in cycle_parts]
226242

227243
prefix_string = ';'.join(canonical_prefix)
228244
if len(cycle_parts) > 0:

src/templates/version.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.9.11
1+
1.9.12

test/test_trace_canonicalization.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@ def test_mermaid_rendering_still_shows_negation_symbol(self):
4343
rendered = node.__mermaid_str__()
4444
self.assertIn("¬b", rendered)
4545

46+
def test_canonicalize_resolves_or_in_state(self):
47+
"""SPOT may output states with | (OR). canonicalizeSpotTrace must resolve
48+
ORs before reordering literals, otherwise the trace gets corrupted."""
49+
trace = "(s & v) | (!s & !v); cycle{1}"
50+
result = canonicalizeSpotTrace(trace)
51+
# The OR should be resolved to one concrete branch, not garbled
52+
self.assertNotIn("|", result.split("cycle")[0],
53+
"OR should be resolved in prefix states")
54+
# Result must be one of the two valid branches
55+
prefix = result.split(";")[0].strip()
56+
self.assertIn(prefix, ["s & v", "!s & !v"],
57+
f"Expected a valid branch, got '{prefix}'")
58+
59+
def test_canonicalize_resolves_or_in_cycle(self):
60+
"""ORs in cycle states must also be resolved."""
61+
trace = "s & v; cycle{(s & v) | (!s & !v)}"
62+
result = canonicalizeSpotTrace(trace)
63+
# Extract cycle content
64+
cycle_part = result.split("cycle{")[1].rstrip("}")
65+
self.assertNotIn("|", cycle_part,
66+
"OR should be resolved in cycle states")
67+
self.assertIn(cycle_part, ["s & v", "!s & !v"],
68+
f"Expected a valid branch in cycle, got '{cycle_part}'")
69+
70+
def test_canonicalize_preserves_simple_traces(self):
71+
"""Traces without OR should be unaffected by the OR-resolution step."""
72+
trace = "!s & v; cycle{s & !v}"
73+
result = canonicalizeSpotTrace(trace)
74+
self.assertEqual(result, "!s & v;cycle{s & !v}")
75+
4676

4777
if __name__ == "__main__":
4878
unittest.main()

0 commit comments

Comments
 (0)