|
2 | 2 |
|
3 | 3 | _Current status file. Most recent session at top._ |
4 | 4 |
|
| 5 | +## 2026-07-09 — Adversarial code review: the v0.4.0 sulcus export was broken. Fixed. |
| 6 | + |
| 7 | +User asked for a full, mean code review of the whole tree, then to fix everything found. Read all |
| 8 | +~3,000 lines of source, then went to `pycortex-src` and read `cortex/svgoverlay.py` — which nobody |
| 9 | +had done during the v0.4.0 build. **The shipped `sulci.svg` could not be read by pycortex at all.** |
| 10 | + |
| 11 | +### The three blockers (all verified against the real parser, all now fixed) |
| 12 | + |
| 13 | +1. **Not well-formed XML.** The exported fragment used the `inkscape:` prefix with no namespace |
| 14 | + declaration and no `<svg>` root. `ET.parse` → `ParseError: unbound prefix`. Nothing — ElementTree, |
| 15 | + lxml, a browser, Inkscape — could open the file. `escapeXml` was fastidiously escaping five |
| 16 | + entities inside a document no parser would accept. |
| 17 | +2. **The `<text data-ptidx>` labels crashed `db.get_overlay()`.** `Labels.__init__` (svgoverlay.py |
| 18 | + ~line 413) does an unguarded `float(text.get('x'))` over **every** `<text>` in a labels layer. |
| 19 | + Ours had no x/y — a vertex index cannot supply one. `TypeError`, before anything rendered. |
| 20 | +3. **`data-ptidx` is pycortex's OUTPUT, not its input.** `SVGOverlay.set_coords` computes it by |
| 21 | + kd-tree from each label's x/y and overwrites whatever is there. `grep -c '<text' S1/overlays.svg` |
| 22 | + → **0**: pycortex stores no labels at all; `Shape.get_labelpos()` derives one per path (so a |
| 23 | + two-hemisphere sulcus is labelled twice for free). The whole `labelForCurve` → `nearestVertexTo` |
| 24 | + → `ptidxFor` → `<text>` chain computed a value pycortex ignores and emitted it in a form that |
| 25 | + made pycortex crash. |
| 26 | + |
| 27 | +Plus a data-loss footgun: README said "paste or merge into overlays.svg". `SVGOverlay` keys layers by |
| 28 | +`inkscape:label`, so appending a second `sulci` layer **silently replaces the subject's own** in |
| 29 | +`self.layers`. You must copy the shape groups into the existing `#sulci_shapes`. |
| 30 | + |
| 31 | +**And `test/svg-export.test.js` never parsed its own output.** Ten tests, all regex-matching the |
| 32 | +string the author meant to write. That is exactly why all three bugs shipped. One |
| 33 | +`ET.fromstring` would have caught #1 on day one. |
| 34 | + |
| 35 | +### Other findings from the same read |
| 36 | + |
| 37 | +- Two more facts the old comments got wrong: `[sulci_paths]` is read **only** by |
| 38 | + `quickflat/composite.py` at render time — `Shape.__init__` seeds style from `[overlay_paths]` and |
| 39 | + `Shape.set()` overwrites every path. So the on-disk `style` matters only to Inkscape. (The v0.4.0 |
| 40 | + header claimed `Overlay.set()` re-applies `[sulci_paths]` at load.) |
| 41 | +- **The stale-index guard covered 2 of 5 edit ops.** `setAnchorSmooth`/`splitSegment` threw |
| 42 | + `TypeError`; `deleteAnchor` silently no-op'd; `setAnchorSmooth(closed, i=9)` grew `smooth[]` to |
| 43 | + length 10 against 4 anchors, with holes — the *exact* desync `inRange` was written to kill. |
| 44 | +- `loadJSON`'s "arrays are defensively copied so the model never aliases the caller's parsed JSON" |
| 45 | + was **false**: `r.bezier` aliased wholesale, `outline.slice()` shared every `{h,g}`. |
| 46 | +- `setOverlayLayer` returns `false` while the SVG overlay loads; nobody checked → a shape listed in |
| 47 | + the panel, counted, and never drawn, with no retry. |
| 48 | +- `_editToggle` accepted a bezier-less shape: `editingId` set, overlay not editing → panel shows |
| 49 | + "✓ Done editing" while the lasso stays armed. Unreachable only because the panel disables the |
| 50 | + button (a UI accident load-bearing for a controller invariant). |
| 51 | +- `_frameOnLoad` (60 × 100 ms), `_download` (4 s), and the adapter's `setData` listener all outlived |
| 52 | + `destroy()` — and `autoAttach` destroys-then-attaches. |
| 53 | +- `"… " + text.length + " bytes"` counts UTF-16 code units. |
| 54 | +- Doc rot: `bezier-edit-overlay.js` "EDITING an ROI's bezier" + `this.roi`; `draw-panel.js` "the ROI |
| 55 | + control panel"; `transform.js` "Used ONLY by the bezier edit overlay" (curveFromTrace uses it). |
| 56 | +- `isClosed(null) === true`; lasso had no degenerate-stroke guard while trace did; `fitHomography` |
| 57 | + checked `spans2D(src)` but not `dst`. |
| 58 | + |
| 59 | +### The fix (18 files, +717/−290; suite 152 → 161 JS tests, all Python green) |
| 60 | + |
| 61 | +- **`core/svg-export.js` rewritten.** Standalone `<svg>` root with both namespaces + the overlay's |
| 62 | + `width`/`height`/`viewBox`. **No labels** — but the `labels` layer is still emitted, empty, because |
| 63 | + `_find_layer(layer, "labels")` raises `ValueError` without it. An XML comment in the file itself |
| 64 | + carries the merge instructions. `SULCI_STROKE_WIDTH`/`_OPACITY` exported and imported by the |
| 65 | + adapter, so the live stroke and the exported stroke can't drift (they were `6` in two places). |
| 66 | +- **`test/test_sulci_svg.py` (new, wired into `npm run test:py`).** Generates the writer's real output |
| 67 | + with node, parses it with ElementTree using svgoverlay.py's own namespaces and `findall` queries. |
| 68 | + Needs no `cortex`, no subject. Runs in CI. |
| 69 | +- All five bezier edit ops share one contract: out-of-range → unchanged copy, never throws. Swept by |
| 70 | + test across every op × bad index × curve kind. |
| 71 | +- `loadJSON` deep-copies (bezier, outline entries, labelVert). `isClosed(null) → false`. |
| 72 | +- `exportSulciMarkup` returns `null` (overlay not loaded) vs `""` (no curve yielded a path); the |
| 73 | + controller reports each correctly. `_sync` polls on `setOverlayLayer` failure. `_editToggle` |
| 74 | + requires a bezier. Timers tracked + cancelled; adapter grew a `destroy()`. |
| 75 | +- `byteLength` via `TextEncoder`. Status strings extracted. Lasso rejects degenerate strokes. |
| 76 | + `fitHomography` checks `dst` too. `roi` → `shape` throughout the edit overlay. |
| 77 | +- **Every new test was mutation-checked**: reverted each fix, confirmed the test fails. |
| 78 | + |
| 79 | +### Open / next time |
| 80 | + |
| 81 | +- **Unchanged and still the top gap:** `svgoverlay.py` itself has never run on roidraw's output. |
| 82 | + Needs a subject + importable `cortex`. The Python test reproduces the parser's *queries*, which is |
| 83 | + as close as CI can get here. |
| 84 | +- The browser `_import` (`FileReader`) and `_download` (`Blob`/anchor/`revokeObjectURL`) paths still |
| 85 | + have zero coverage; the 4 s teardown exists for Firefox and is untested. |
| 86 | +- **The 2026-07-09 CDP live-viewer check predates this rewrite** — it validated the old broken |
| 87 | + markup. Re-run it before the next release. |
| 88 | +- **A new release is needed**: `dist/` is gitignored, so `/releases/latest` still serves the v0.4.0 |
| 89 | + bundle with the broken exporter. Cut v0.4.1, then re-bake the demo viewer (see |
| 90 | + [[roidraw-release-artifact-ordering]]). pycortex docs PR #656 is still OPEN. |
| 91 | +- Lesson recorded in memory as `string-tests-cannot-check-a-format`. |
| 92 | + |
5 | 93 | ## 2026-07-08/09 — Sulcus drawing (v0.4.0): spec → plan → 12 TDD tasks → SHIPPED (release + demo + docs PR) |
6 | 94 |
|
7 | 95 | User asked to add sulcus + gyrus drawing. Brainstormed; **researched how pycortex actually stores |
|
0 commit comments