diff --git a/docs/superpowers/evidence/calc-live-split-2026-08-16.json b/docs/superpowers/evidence/calc-live-split-2026-08-16.json new file mode 100644 index 00000000..7cdcfc17 --- /dev/null +++ b/docs/superpowers/evidence/calc-live-split-2026-08-16.json @@ -0,0 +1,48 @@ +{ + "intent": "Bring calc-live.zsh under the 250-line cap by extracting the warmed qalc coprocess into a sibling file, without changing what the live prompt calculator computes.", + "scope": [ + "linux_configuration/zsh/calc-live.zsh (266 -> 204 lines)", + "New: linux_configuration/zsh/calc-live-qalc.zsh (74 lines)", + "Non-goal: changing the evaluator, the expression heuristic, or the ZLE bindings" + ], + "changes": [ + "Moved the qalc coprocess block -- its four _CALC_Q* state variables and _calc_qalc_line, _calc_qco_start, _calc_qco_stop, _calc_qalc_eval -- into calc-live-qalc.zsh, sourced from calc-live.zsh after _CALC_QALC_BIN and _CALC_TIMEOUT_BIN are set.", + "The source line uses ${0:A:h}, the same idiom the file already relies on for _CALC_EVAL_PY. This matters here more than usual: the file is loaded as ~/.oh-my-zsh/custom/calc-live.zsh, a symlink into the repo, so a naive $0 would resolve into the oh-my-zsh custom directory where the sibling does not exist." + ], + "verification": [ + { + "command": "zsh -c 'source ~/.oh-my-zsh/custom/calc-live.zsh; ...' -- through the real symlink, not the repo path", + "result": "pass", + "evidence": "Sources without error, all five probed functions are defined across the seam, _calc_eval '10/4' returns 2.5, and the qalc path in the NEW file works end to end: _calc_qalc_line '=5 ft to cm' strips to '5 ft to cm' and _calc_qalc_eval returns '152.4 cm'. This is the check that matters, since the symlink is how the file is actually loaded." + }, + { + "command": "seven expressions evaluated under HEAD and under the split, compared", + "result": "pass", + "evidence": "Identical for all seven: 2+2=4, 1/3=0.333333333333, sqrt(2)*3=4.24264068712, sin(pi/2)=1, 2^10=1024, 10/4=2.5, factorial(5)=120. _calc_is_expr still accepts '2+2' and still rejects 'ls -la', so ordinary commands remain untouched." + }, + { + "command": "zsh -n on both files", + "result": "pass", + "evidence": "Both parse. zsh is the right parser here; shellcheck cannot read zsh syntax and the repo's shellcheck hook excludes zsh files, so the zsh-syntax hook is the gate." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh", + "result": "pass", + "evidence": "calc-live.zsh 204, calc-live-qalc.zsh 74. Neither appears in the violation list." + }, + { + "command": "function-set and line-set diff against HEAD", + "result": "pass", + "evidence": "All ten functions preserved and identical. Zero executable lines lost; the single addition is the source line." + } + ], + "risks": [ + "This is a live interactive component: a broken source line would break every new shell, not just a script run on demand. That is why it was verified through the oh-my-zsh symlink rather than the repo path -- the symlink is the only path that actually exercises ${0:A:h}.", + "The coprocess variables are shared across the seam by design: calc-live-qalc.zsh sets _CALC_QRESULT and _CALC_QEXPR, which _calc_preview and _calc_accept_line in the main file read. This is a sourced-into-the-same-shell relationship, not two processes, and the end-to-end qalc test exercises exactly that path.", + "Already-running shells keep the old single-file version in memory until they exit; nothing needs restarting, but a shell opened mid-commit could source a half-written pair. Both files were written before either was staged." + ], + "rollback": [ + "git revert the commit; calc-live.zsh returns to its 266-line single-file form and the sibling disappears. No symlink change is needed -- the oh-my-zsh symlink points at calc-live.zsh either way.", + "After rollback, open a new shell and type '2+2' to confirm the live preview still appears." + ] +} diff --git a/linux_configuration/zsh/calc-live-qalc.zsh b/linux_configuration/zsh/calc-live-qalc.zsh new file mode 100644 index 00000000..d8131110 --- /dev/null +++ b/linux_configuration/zsh/calc-live-qalc.zsh @@ -0,0 +1,74 @@ +# calc-live-qalc.zsh — the warmed qalc coprocess for "="-prefixed lines +# ---------------------------------------------------------------------------- +# Sourced by calc-live.zsh; split out to keep both files under the 250-line +# cap. Not standalone: it relies on _CALC_QALC_BIN and _CALC_TIMEOUT_BIN, which +# calc-live.zsh sets above the source line. + +# --- qalc (richer engine) for "="-prefixed lines: units, hex, percentages ---- +# qalc cold start is ~120 ms, far too slow per keystroke; but a warmed qalc +# coprocess answers in ~1 ms, fast enough to evaluate live in the redraw hook. +# So we keep ONE persistent qalc per shell, started lazily on first "=" line. +# +# IMPORTANT: the coprocess lives in the main shell. Its evaluator must NOT be +# called via $(...) (that forks a subshell and loses the pipe) — it sets the +# global _CALC_QRESULT instead. +typeset -g _CALC_QCO_UP=0 # 1 once the qalc coprocess is running +typeset -gi _CALC_QSEQ=0 # unique-sentinel counter (prevents desync) +typeset -g _CALC_QEXPR= # stripped expression from the last "=" line +typeset -g _CALC_QRESULT= # result from the last _calc_qalc_eval + +# If the line is "=", set _CALC_QEXPR to the stripped and return 0. +_calc_qalc_line() { + emulate -L zsh + setopt local_options extended_glob + local s=$1 + _CALC_QEXPR= + [[ $s == (#s)[[:space:]]#=* ]] || return 1 + s=${s##[[:space:]]#=[[:space:]]#} # drop leading spaces, the "=", trailing spaces + [[ -n $s ]] || return 1 + _CALC_QEXPR=$s + return 0 +} + +# Start / stop the persistent qalc coprocess (no job-control chatter). +_calc_qco_start() { + setopt local_options no_monitor no_notify + coproc "$_CALC_QALC_BIN" -t 2>/dev/null + _CALC_QCO_PID=$! + disown %+ 2>/dev/null + _CALC_QCO_UP=1 +} +_calc_qco_stop() { + [[ ${_CALC_QCO_UP:-0} == 1 ]] || return 0 + kill "${_CALC_QCO_PID:-0}" 2>/dev/null + _CALC_QCO_UP=0 +} + +# Evaluate _CALC_QEXPR (arg) via the coprocess; sets _CALC_QRESULT ("" on fail). +_calc_qalc_eval() { + emulate -L zsh + setopt local_options extended_glob no_monitor no_notify + _CALC_QRESULT= + [[ -n $_CALC_QALC_BIN ]] || return 0 + local expr=${1// of / * } # qalc mis-parses "A% of B" + (( _CALC_QCO_UP )) || _calc_qco_start + (( _CALC_QSEQ++ )) + local sentinel="909090909${_CALC_QSEQ}" # unique per call, echoes itself + if ! { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null; then + _calc_qco_stop; _calc_qco_start # pipe broke -> restart once + { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null || return 0 + fi + local line result='' saw=0 + integer guard=0 + while (( guard++ < 100 )) && read -rp -t 0.8 line 2>/dev/null; do + line=${line//$'\e'\[[0-9;?]##[a-zA-Z]/} # strip color escapes + line=${line##[[:space:]]##}; line=${line%%[[:space:]]##} + [[ -z $line || $line == '>'* ]] && continue # blank / echoed input + [[ $line == *$sentinel* ]] && { saw=1; break; } # our sentinel result + result=$line + done + (( saw )) || { _CALC_QCO_UP=0; return 0 } # timed out/wedged -> reset + # qalc echoes the input back when it cannot evaluate (e.g. "1 / 0"). + [[ -n $result && ${result// /} != ${expr// /} ]] && _CALC_QRESULT=$result + return 0 +} diff --git a/linux_configuration/zsh/calc-live.zsh b/linux_configuration/zsh/calc-live.zsh index ce02bf1a..0ae15e7c 100644 --- a/linux_configuration/zsh/calc-live.zsh +++ b/linux_configuration/zsh/calc-live.zsh @@ -84,74 +84,12 @@ _calc_eval() { print -r -- "$out" } -# --- qalc (richer engine) for "="-prefixed lines: units, hex, percentages ---- -# qalc cold start is ~120 ms, far too slow per keystroke; but a warmed qalc -# coprocess answers in ~1 ms, fast enough to evaluate live in the redraw hook. -# So we keep ONE persistent qalc per shell, started lazily on first "=" line. -# -# IMPORTANT: the coprocess lives in the main shell. Its evaluator must NOT be -# called via $(...) (that forks a subshell and loses the pipe) — it sets the -# global _CALC_QRESULT instead. -typeset -g _CALC_QCO_UP=0 # 1 once the qalc coprocess is running -typeset -gi _CALC_QSEQ=0 # unique-sentinel counter (prevents desync) -typeset -g _CALC_QEXPR= # stripped expression from the last "=" line -typeset -g _CALC_QRESULT= # result from the last _calc_qalc_eval +# --- qalc (richer engine) for "="-prefixed lines ------------------------------ +# Sourced after _CALC_QALC_BIN and _CALC_TIMEOUT_BIN above, which it reads. +# ${0:A:h} follows the oh-my-zsh symlink back to the real directory in the repo, +# the same idiom used for _CALC_EVAL_PY above. +source ${0:A:h}/calc-live-qalc.zsh -# If the line is "=", set _CALC_QEXPR to the stripped and return 0. -_calc_qalc_line() { - emulate -L zsh - setopt local_options extended_glob - local s=$1 - _CALC_QEXPR= - [[ $s == (#s)[[:space:]]#=* ]] || return 1 - s=${s##[[:space:]]#=[[:space:]]#} # drop leading spaces, the "=", trailing spaces - [[ -n $s ]] || return 1 - _CALC_QEXPR=$s - return 0 -} - -# Start / stop the persistent qalc coprocess (no job-control chatter). -_calc_qco_start() { - setopt local_options no_monitor no_notify - coproc "$_CALC_QALC_BIN" -t 2>/dev/null - _CALC_QCO_PID=$! - disown %+ 2>/dev/null - _CALC_QCO_UP=1 -} -_calc_qco_stop() { - [[ ${_CALC_QCO_UP:-0} == 1 ]] || return 0 - kill "${_CALC_QCO_PID:-0}" 2>/dev/null - _CALC_QCO_UP=0 -} - -# Evaluate _CALC_QEXPR (arg) via the coprocess; sets _CALC_QRESULT ("" on fail). -_calc_qalc_eval() { - emulate -L zsh - setopt local_options extended_glob no_monitor no_notify - _CALC_QRESULT= - [[ -n $_CALC_QALC_BIN ]] || return 0 - local expr=${1// of / * } # qalc mis-parses "A% of B" - (( _CALC_QCO_UP )) || _calc_qco_start - (( _CALC_QSEQ++ )) - local sentinel="909090909${_CALC_QSEQ}" # unique per call, echoes itself - if ! { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null; then - _calc_qco_stop; _calc_qco_start # pipe broke -> restart once - { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null || return 0 - fi - local line result='' saw=0 - integer guard=0 - while (( guard++ < 100 )) && read -rp -t 0.8 line 2>/dev/null; do - line=${line//$'\e'\[[0-9;?]##[a-zA-Z]/} # strip color escapes - line=${line##[[:space:]]##}; line=${line%%[[:space:]]##} - [[ -z $line || $line == '>'* ]] && continue # blank / echoed input - [[ $line == *$sentinel* ]] && { saw=1; break; } # our sentinel result - result=$line - done - (( saw )) || { _CALC_QCO_UP=0; return 0 } # timed out/wedged -> reset - # qalc echoes the input back when it cannot evaluate (e.g. "1 / 0"). - [[ -n $result && ${result// /} != ${expr// /} ]] && _CALC_QRESULT=$result - return 0 -} # --- Decide whether a line is a pure math expression worth evaluating -------- # Conservative on purpose: must contain a digit and an operator (or a function