|
| 1 | +# TASK-091: Oracle Parity — Fractional index algorithm |
| 2 | + |
| 3 | +## Status |
| 4 | +- [ ] Planned |
| 5 | +- [ ] Assigned |
| 6 | +- [ ] In Progress |
| 7 | +- [ ] Blocked (reason: ...) |
| 8 | +- [x] Complete |
| 9 | + |
| 10 | +## Priority |
| 11 | +medium |
| 12 | + |
| 13 | +## Assigned To |
| 14 | +(unassigned) |
| 15 | + |
| 16 | +## Parent Docs / Cross-links |
| 17 | +- Rust fract implementation: `core/rs/fractindex-core/src/fractindex.rs` |
| 18 | +- Zig fract implementation: `zig/src/fract_index.zig` |
| 19 | +- Existing fract tests: `zig/harness/test-fract.sh` |
| 20 | +- Gap backlog: `research/zig-cr/92-gap-backlog.md` |
| 21 | + |
| 22 | +## Description |
| 23 | +Verify that `crsql_fract_key_between(a, b)` produces identical output in both Rust/C and Zig for the same inputs. |
| 24 | + |
| 25 | +This is an **oracle test**: The fractional index algorithm must be deterministic and produce the same lexicographically-sortable string in both implementations. |
| 26 | + |
| 27 | +## Files to Modify |
| 28 | +- `zig/harness/test-fract-parity.sh` (new or extend `test-fract.sh`) |
| 29 | +- `zig/harness/test-parity.sh` (wire into suite) |
| 30 | +- `research/zig-cr/92-gap-backlog.md` |
| 31 | + |
| 32 | +## Acceptance Criteria |
| 33 | +- [x] Test calls `crsql_fract_key_between(a, b)` with identical inputs on both implementations. |
| 34 | +- [x] Test cases include: |
| 35 | + - `(NULL, NULL)` — first key: returns `'a '` (hex: 6120) |
| 36 | + - `('a ', NULL)` — key after 'a ': returns `'a!'` (hex: 6121) |
| 37 | + - `(NULL, 'a ')` — key before 'a ': returns `'Z~'` (hex: 5A7E) |
| 38 | + - `('a0', 'a1')` — key between: returns `'a0P'` (hex: 613050) |
| 39 | + - `('aaa', 'aab')` — key between close values: returns `'aaaP'` (hex: 61616150) |
| 40 | + - `('', 'a ')` — edge case with empty string: both reject as invalid |
| 41 | + - Long strings (101 chars) to test: returns `'aQ'` (hex: 6151) |
| 42 | +- [x] Outputs are **byte-identical** (not just semantically equivalent). |
| 43 | +- [x] Test fails if any output differs. |
| 44 | +- [x] Results maintain lexicographic ordering: `a < result < b` when both are non-NULL. |
| 45 | + |
| 46 | +## Progress Log |
| 47 | +### 2025-12-18 |
| 48 | +- Task created from oracle-based parity test suite. |
| 49 | + |
| 50 | +### 2025-12-18 |
| 51 | +- Created `zig/harness/test-fract-parity.sh` with 12 test cases |
| 52 | +- Wired into `zig/harness/test-parity.sh` |
| 53 | +- All 12 parity tests pass - Zig and Rust/C produce byte-identical output |
| 54 | + |
| 55 | +## Completion Notes |
| 56 | + |
| 57 | +### Commands Run |
| 58 | +```bash |
| 59 | +# Direct test run |
| 60 | +./zig/harness/test-fract-parity.sh |
| 61 | + |
| 62 | +# Via make target |
| 63 | +make -C zig test-parity |
| 64 | +``` |
| 65 | + |
| 66 | +### Full Test Output |
| 67 | +``` |
| 68 | +╔═══════════════════════════════════════════════════════════════════════╗ |
| 69 | +║ Fractional Index Oracle Parity Test ║ |
| 70 | +║ Compares Rust/C vs Zig implementation of crsql_fract_key_between ║ |
| 71 | +╚═══════════════════════════════════════════════════════════════════════╝ |
| 72 | +
|
| 73 | +Rust/C extension: lib/crsqlite.dylib |
| 74 | +Zig extension: lib/crsqlite-zig-darwin-aarch64.dylib |
| 75 | +
|
| 76 | +Test: (NULL, NULL) — first key |
| 77 | + Rust/C: 'a ' (hex: 6120) |
| 78 | + Zig: 'a ' (hex: 6120) |
| 79 | + ✓ Byte-identical: YES |
| 80 | + PASS |
| 81 | +
|
| 82 | +Test: ('a ', NULL) — key after 'a ' |
| 83 | + Rust/C: 'a!' (hex: 6121) |
| 84 | + Zig: 'a!' (hex: 6121) |
| 85 | + ✓ Byte-identical: YES |
| 86 | + PASS |
| 87 | +
|
| 88 | +Test: (NULL, 'a ') — key before 'a ' |
| 89 | + Rust/C: 'Z~' (hex: 5A7E) |
| 90 | + Zig: 'Z~' (hex: 5A7E) |
| 91 | + ✓ Byte-identical: YES |
| 92 | + PASS |
| 93 | +
|
| 94 | +Test: ('a0', 'a1') — key between |
| 95 | + Rust/C: 'a0P' (hex: 613050) |
| 96 | + Zig: 'a0P' (hex: 613050) |
| 97 | + ✓ Byte-identical: YES |
| 98 | + ✓ Ordering: a < result < b |
| 99 | + PASS |
| 100 | +
|
| 101 | +Test: ('aaa', 'aab') — close values |
| 102 | + Rust/C: 'aaaP' (hex: 61616150) |
| 103 | + Zig: 'aaaP' (hex: 61616150) |
| 104 | + ✓ Byte-identical: YES |
| 105 | + ✓ Ordering: a < result < b |
| 106 | + PASS |
| 107 | +
|
| 108 | +Test: ('a0P', 'a0Q') — very close values |
| 109 | + Rust/C: 'a0PP' (hex: 61305050) |
| 110 | + Zig: 'a0PP' (hex: 61305050) |
| 111 | + ✓ Byte-identical: YES |
| 112 | + ✓ Ordering: a < result < b |
| 113 | + PASS |
| 114 | +
|
| 115 | +Test: Long string (101 chars) |
| 116 | + Rust/C: 'aQ' (hex: 6151) |
| 117 | + Zig: 'aQ' (hex: 6151) |
| 118 | + ✓ Byte-identical: YES |
| 119 | + PASS |
| 120 | +
|
| 121 | +Test: (NULL, 'Z~') — negative integer |
| 122 | + Rust/C: 'Z}' (hex: 5A7D) |
| 123 | + Zig: 'Z}' (hex: 5A7D) |
| 124 | + ✓ Byte-identical: YES |
| 125 | + PASS |
| 126 | +
|
| 127 | +Test: ('Z}', 'Z~') — between negative integers |
| 128 | + Rust/C: 'Z}P' (hex: 5A7D50) |
| 129 | + Zig: 'Z}P' (hex: 5A7D50) |
| 130 | + ✓ Byte-identical: YES |
| 131 | + ✓ Ordering: a < result < b |
| 132 | + PASS |
| 133 | +
|
| 134 | +Test: Sequential key generation maintains ordering |
| 135 | + Rust/C sequence (hex): 6120,6121,6122,6123,6124 |
| 136 | + Zig sequence (hex): 6120,6121,6122,6123,6124 |
| 137 | + ✓ Byte-identical: YES |
| 138 | + PASS |
| 139 | +
|
| 140 | +Test: Error case parity - empty string |
| 141 | + Rust/C value: ERROR (rejects invalid input) |
| 142 | + Zig value: ERROR (rejects invalid input) |
| 143 | + ✓ Both reject invalid input (empty string) |
| 144 | + PASS |
| 145 | +
|
| 146 | +Test: Error case parity - a > b (invalid order) |
| 147 | + Rust/C: Error (must be before) |
| 148 | + Zig: Error (left key must be before right key) |
| 149 | + ✓ Both produce error on invalid order |
| 150 | + PASS |
| 151 | +
|
| 152 | +╔═══════════════════════════════════════════════════════════════════════╗ |
| 153 | +║ PARITY TEST SUMMARY ║ |
| 154 | +╠═══════════════════════════════════════════════════════════════════════╣ |
| 155 | +║ PASSED: 12 ║ |
| 156 | +║ FAILED: 0 ║ |
| 157 | +╚═══════════════════════════════════════════════════════════════════════╝ |
| 158 | +
|
| 159 | +✓ All parity tests PASSED - Zig and Rust/C are byte-identical |
| 160 | +``` |
| 161 | + |
| 162 | +### Divergences Discovered |
| 163 | +None - all outputs are byte-identical between Zig and Rust/C implementations. |
| 164 | + |
| 165 | +### Notes on Test Input Format |
| 166 | +The fractional index algorithm uses a specific key format with an "integer part" prefix. Valid keys must: |
| 167 | +- Start with a letter A-Z (negative integers) or a-z (positive integers) |
| 168 | +- Have a fractional part using base-95 digits (space to tilde) |
| 169 | + |
| 170 | +Invalid inputs like `'a'` (missing fractional part) or `''` (empty string) are rejected by both implementations. |
0 commit comments