Fix X-Stitch-Count off-by-one (issue #10) #18

Closed
Mark wants to merge 4 commits from fix/10-stitch-count-off-by-one into main
Owner

Summary

Fixes the off-by-one between X-Stitch-Count and the on-disk stitch record count reported in Mark/easystitch-service#10.

The header was previously computed from sum(len(obj.stitches) for obj in req.stitch_objects) (pre-fix app.py:242), which only counted the stitch points in the request payload. pyembroidery's writer unconditionally appends a trailing END record on disk (and PES injects additional JUMP/TRIM markers around color changes via FULL_JUMP = True), so the on-disk record count is strictly greater than the request-payload point count.

Fix

X-Stitch-Count is now derived from a round-trip read of the encoded bytes: after write_<fmt>(pattern, buf) returns, the buffer is re-decoded through the matching reader and the header is set to len(roundtrip.stitches). This makes the header authoritative — by construction it equals what pyembroidery.read(...) would return from the file bytes — without changing the on-disk file format.

Reproduction (before fix)

For the same digitize response, X-Stitch-Count: 139916 vs len(pyembroidery.read(file).stitches) == 139917 (off by 1; the trailing END record). See parent card t_6acb4d16 investigation report for the full PES/DST/EXP/JEF single-color and multi-color breakdown.

Diff

app.py                    | 18 ++++++++++++++++-
tests/unit/test_export.py | 49 +++++++++++++++++++++++++++++++++--------------
2 files changed, 52 insertions(+), 15 deletions(-)

Tests

  • New regression test test_export_header_matches_pes_byte_count asserts the header equals the round-tripped file's stitch count for PES. Fails on main (off-by-one), passes after the fix.
  • test_export_round_trips_each_format and test_export_multi_color_inserts_color_changes now assert against len(pyembroidery.read(path).stitches) instead of the request-payload sum.
  • Full unit suite: 86 passed. ruff check app.py tests/unit/test_export.py: clean.

Closes Mark/easystitch-service#10

## Summary Fixes the off-by-one between `X-Stitch-Count` and the on-disk stitch record count reported in Mark/easystitch-service#10. The header was previously computed from `sum(len(obj.stitches) for obj in req.stitch_objects)` (pre-fix `app.py:242`), which only counted the *stitch points* in the request payload. pyembroidery's writer unconditionally appends a trailing `END` record on disk (and PES injects additional `JUMP`/`TRIM` markers around color changes via `FULL_JUMP = True`), so the on-disk record count is strictly greater than the request-payload point count. ## Fix `X-Stitch-Count` is now derived from a round-trip read of the encoded bytes: after `write_<fmt>(pattern, buf)` returns, the buffer is re-decoded through the matching reader and the header is set to `len(roundtrip.stitches)`. This makes the header authoritative — by construction it equals what `pyembroidery.read(...)` would return from the file bytes — without changing the on-disk file format. ## Reproduction (before fix) For the same digitize response, `X-Stitch-Count: 139916` vs `len(pyembroidery.read(file).stitches) == 139917` (off by 1; the trailing `END` record). See parent card `t_6acb4d16` investigation report for the full PES/DST/EXP/JEF single-color and multi-color breakdown. ## Diff ``` app.py | 18 ++++++++++++++++- tests/unit/test_export.py | 49 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 15 deletions(-) ``` ## Tests - New regression test `test_export_header_matches_pes_byte_count` asserts the header equals the round-tripped file's stitch count for PES. Fails on `main` (off-by-one), passes after the fix. - `test_export_round_trips_each_format` and `test_export_multi_color_inserts_color_changes` now assert against `len(pyembroidery.read(path).stitches)` instead of the request-payload sum. - Full unit suite: 86 passed. `ruff check app.py tests/unit/test_export.py`: clean. Closes Mark/easystitch-service#10
Fix X-Stitch-Count off-by-one (issue #10)
Some checks failed
CI / test (pull_request) Failing after 5s
CI / docker-build (pull_request) Has been skipped
3d11f258b2
Derive X-Stitch-Count from a round-trip read of the encoded bytes
rather than the request payload. pyembroidery's writers append a
trailing END (and PES injects JUMP/TRIM around color changes), so
len(pattern.stitches) before write does not match the on-disk record
count. Use EmbPattern.read_embroidery on the BytesIO buffer to get the
authoritative count after the writer has run.

Tests now assert header == len(pyembroidery.read(path).stitches) and a
new regression test test_export_header_matches_pes_byte_count locks
in the fix.
Mark/easystitch-service#16

The verify gate halts at slice 1: the deployed hostname
easystitch-service.tail018ac4.ts.net does not resolve from this runner
(NXDOMAIN), and the easystitch-service app is not registered in
nodehub's /api/apps. Local-only checks (ruff + pytest tests/unit/)
still pass. _shakedown_output/ingress_report.md captures the verdict
and evidence; it lives under _shakedown_output/ which is gitignored.
docs(plan): add diagnostic plan + report for issue #17 launcher stdout capture
Some checks failed
CI / test (pull_request) Failing after 1s
CI / docker-build (pull_request) Has been skipped
c0e52ed43c
Issue #17 is a diagnostic: verify that the kilo-gsd-runner launcher's
terminal-tool channel round-trips stdout to the agent's tool-result surface.
No source code in easystitch-service is modified.

Six probes (trivial, multi-line, 4 KB size floor, stderr separability,
non-zero exit, JSON parseable) were executed and transcribed into
_shakedown_output/launcher_stdout_diagnostic.md (gitignored).

Findings: 5 PASS, 1 FAIL (informational) — stderr and stdout are merged in
the tool result with no stream annotation. No prior shakedown depends on
stderr/stdout distinguishability, so this does not invalidate any earlier
verify gate.
Mark closed this pull request 2026-08-30 21:30:36 +00:00
Some checks failed
CI / test (pull_request) Failing after 1s
CI / docker-build (pull_request) Has been skipped

Pull request closed

Sign in to join this conversation.
No description provided.