Stage 3 prompt stream never updates handheld UI (#19 blocker) #50
Notifications
Due Date
No due date set.
Blocks
#18 Instrument detection (`instlist`)
gronod/iccery-v2-mac
#20 Live swatch grid & ΔE₀₀
gronod/iccery-v2-mac
#22 XY-table flow UI
gronod/iccery-v2-mac
Reference: gronod/iccery-v2-mac#50
Reference in New Issue
Block a user
Summary
runChartreaddrops handheld prompt events, so Calibrate / Trigger / Done & Save never appear. This is whyMilestone4UITests.testHandheldFixtureChartreadAndAverageis skipped. Same ticket also covers the classifier-order and Stage 3 chrome defects that will fire as soon as prompts work.Spec refs
docs/15-stage3-chartread.md (state machine, buttons vs keys, 39 classifier tests, #93 sticky XY).
docs/05-argyll-fork.md §12.4–12.6 (real C prompt strings, matcher priority).
docs/03-ipc-and-process-manager.md (stdin independent of wait; killAll on quit; XY
q\n+ 500 ms + kill).#175 Done is
d. #137 send the key the prompt asks. #147/#149 park-before-kill.Open parent: #19. Related: #20 #22.
Scope
In:
ArgyllRunner.runChartread(compare previous state).ChartreadClassifiermatcher order + 39-fixture port from v0.8.5 / docs/05 §12.6.canFinishwired.killAll.testHandheldFixtureChartreadAndAverage.Out:
Implementation notes
1. Prompt emission (root cause)
Packages/ICCeryCore/Sources/ICCeryCore/Argyll/ArgyllRunner.swiftrunChartread:previousOrContinuationStatereturns the new state unless the line is an XY continuation. Handheld transitions therefore compare.calibrating != .calibratingand never yield.prompt.MeasurementWorkflowViewModel.handleonly moveschartreadStateon.prompt/ final JSON row /.completed. Result: UI stays.idle. Calibrate never shows. Fixture test skipped as “timing.”Fix:
Still emit
.removeSheetNoticewithout forcing a state change (#93 info-only). Keep stickyTABLE_*on continuation lines.2. Classifier priority
ChartreadClassifiertreats"read strip"as awaiting-strip before the error matcher, and the error matcher accepts bare"error"/"failed"."failed to read strip"/"error reading strip"→.awaitingStrip, not.error.Port the 39 legacy cases + real C strings from docs/05 §12.6. Matcher order must match that table. Drop substring
"read strip"as a standalone awaiting token; use the real “hit any key to read strip” / “ready to read strip” family. Error matcher: word-boundaryerror/failed to read/too fast/too slow; excludeno error.3. Stage 3 chrome
Stage3View.controlButtonsdraws Done & Save twice on.allStripsReadand Retry twice on.error. TwobtnDoneRead/btnRetryidentifiers.One button per state:
" \n"" \n""\n"or"{key}\n""d\n"" \n"q\n+ 500 ms + kill; else killNo Skip / Undo. Do not also show Done & Save while
.awaitingStripin the same HStack as Trigger (Argyll accepts earlyd; if we keep it, give it a distinct idbtnDoneReadEarly).Wire
btnFinishAndAveragetocanFinish(isFinished && !passSnapshots.isEmpty), notisFinishedalone. Snapshot failure must not offer Finish.4. Child lifetime
startChartread: cancel +cancelChartreadany live session for the same basename before spawn.runChartreadonTermination:cancelChartread(park if XY), do not onlytask.cancel().applicationShouldTerminate: if a chartread id is live and the current instrument is XY,q\n+ 500 ms thenkillAll(). Today it onlykillAll().Optional but recommended: timeout
collect/runChartreadif both pipe EOFs never arrive afterterminate()so a hungreadabilityHandlercannot wedge Stage 3.Rewrite invariants
#175 Done is
d\n, not EOF, not mocks/u.#137 Send the key the prompt asks (
requestedWarningKey).#93 Sticky TABLE_* ; “remove last sheet” is info-only.
#147/#149 Park XY before kill;
killAllon last-window / quit.#116 Exclusive id
chartread_{basename}— a second Start must not collide with a zombie child.#84 stdin handle independent of wait.
Dependencies
Blocks-on: #2, #18 (process id + instlist already on develop).
Unblocks: closing #19 CI/mock gate; unskips M4 UI test; makes #20 live rows reachable; #22 badge script can run.
Test
"failed to read strip"→.error;"Hit [Space] to read strip A"→.awaitingStrip;"Please remove last sheet from table"preserves state +isRemoveSheetNotice..prompt(.calibrating); strip line yields.prompt(.awaitingStrip); done line yields.prompt(.allStripsRead).{basename}.ti3only afterd\n; snapshot then deletes canonical.testHandheldFixtureChartreadAndAverage.Acceptance criteria
.ti3appears only afterd\n.s/u.btnDoneRead/btnRetryin the tree for a given state.Finish & Averagedisabled until a pass snapshot exists.ChartreadClassifierTests.M5 follow-up —
feat/23-colprof/ PR #51#50 body is still correct. M5 did not touch
runChartreadprompt emission,ChartreadClassifier,Stage3View.controlButtons, orcanFinish. Handheld Calibrate / Trigger / Done still never appear. Do not close #50, #19, or M4 because Milestone 5 UI tests are green — those tests pre-stagemytarget.ti3and skip Stage 3 entirely (Tests/ICCeryUITests/Milestone5UITests.swift).Contradictions with the #50 body (explicit)
Scope Out: “Print / ColorSync.” Keep ColorSync print (
lp/ PDE / #14) out of #50. M5 does write ColorSync profile install (~/Library/ColorSync/Profiles,/Library/ColorSync/Profiles). Install defects below are out of original #50 scope. Either expand Scope In with a dated note, or file a separate M5 bug. Do not silently treat them as in-scope.§4 child lifetime is chartread/XY-only. That was right for M4. M5 adds streaming
colprof_{basename},iccgamut_{stem},profcheck_{ti3Path}(all via the samecollect()), plus capturedapplycal_{stem}. Update §4: quit / last-window mustkillAllevery child, not “if chartread and XY thenq\n.” XY park remains a chartread-only prefix; thenkillAll().Captured
killpremature.exitwas listed as “smaller.” Upgrade. M5runApplycaltreats captured exit 0 + tmp exists as success, thenreplaceItemAtover the real.icc.ProcessManager.killon a captured job emits.exitwithterminationStatusbefore the process has died (often0). A quit or duplicate-id kill mid-applycalcan replace a good profile with a truncated{input}.icc.applycal.tmp. That is data loss, not a log glitch.collect()EOF hang was “optional.” Upgrade to required.runColprof/runIccgamut/runProfcheckall sit incollect()untilexitand both pipe EOFs.colprofis the long child (cLUT, minutes, thousands of.with no newline). A hungreadabilityHandlernow wedges Stage 4/5, not only Stage 3. One timeout oncollect()/maybeFinalize, used by every streaming runner.Do not implement #25’s “cards show 0.00” as part of #50. #50 Scope Out already excludes the ΔE formula. M5 Stage 5
metricCardrenders nil as"—", not"0.00"(Stage5View.swift). That satisfies “never silently zero.” Changing those cards to0.00while fixing #50 would regress #25.#50 AC “unskip handheld fixture” is not implied by M5.
testBuildProfileAndVerifyis not a substitute. Leave the M4 skip in place until the prompt-stream fix is in.Still do (unchanged from #50 body)
ChartreadStatebefore assigning, then yield.prompt."read strip"as awaiting.btnDoneRead/btnRetry; wirebtnFinishAndAveragetocanFinish.chartread_{basename}before respawn; streamonTerminationmust kill (park if XY).Add to #50 (same mechanism, M5 made it worse)
A.
collect()must time out and must not require both EOFs forever.Shared by chartread, colprof, iccgamut, profcheck. If
pendingExitCodeis set, finalize after a short drain (e.g. 1–2 s) even if a pipe never delivers emptyavailableData.B. Captured kill must not look like success.
ProcessManager.killoncaptured[id]: wait forterminationHandler(orwaitUntilExit) and emit that code (SIGTERM→ non-zero).runApplycalmust notreplaceItemAtunlessexitCode == 0and tmp size ≥ 128 (same floor as installer). On any kill/cancel, delete tmp and leave the original.icc.C.
createProfile/verifyProfile/runColprofmust not collide on process id.Same exclusive-id rule as chartread (#116). Cancel+kill the live
colprof_*/profcheck_*before a second Start. TodayisColprofRunninggates the button; a stage switch or a stuckcollect()leaves the id leased and the next Create fails withduplicateID.D.
colprofprogress is line-based; the tool emits dots.ProcessLineDecoderbuffers until0x0A. A multi-minute'.....'line never reachesColprofProgressClassifier(only the last line of a flushed batch is classified). Not a Stage 3 bug, but it is the same decoder #50 already depends on. While touching the decoder for EOF timeout, flush a tail of.periodically or classify incomplete lines for colprof only. Do not hop@MainActorper dot (AGENTS.md).