ekp/tests/ekp-gui-verify.el
2026-08-02 15:30:12 +08:00

2250 lines
96 KiB
EmacsLisp
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; ekp-gui-verify.el --- GUI pixel-fit verification for ekp -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Kinney Zhang
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Ground-truth verification that justified text really fits the
;; window, measured with `window-text-pixel-size' on the live display
;; — the one thing batch tests cannot check.
;;
;; Two ways to run:
;;
;; M-x ekp-gui-verify in ANY running GUI Emacs — including your
;; fully customized session. Use this when
;; justified text looks truncated: it tells
;; you whether measurement matches rendering
;; under your fonts/remappings.
;;
;; ${EMACS} -Q -L . -L tests -l tests/ekp-gui-verify.el \
;; -f ekp-gui-verify-matrix
;; runs the full matrix (plain, text-scale
;; up/down, face remap, narrow+scale) and
;; prints a PASS/FAIL table.
;;
;; Criterion: every justified line's rendered width equals the target
;; width (± `ekp-buffer-margin-pixel'). Verbatim paragraphs
;; (`ekp-verbatim') are exempt — code blocks pass through unwrapped by
;; design and may exceed a narrow window, like any code line.
;;; Code:
(require 'ekp)
(require 'ekp-buffer)
(require 'ekp-showcase)
(require 'ert)
(require 'subr-x)
(defun ekp-gui-verify--line-width (window base line)
"Measure planned LINE in WINDOW at paragraph BASE."
(car (window-text-pixel-size
window
(+ base (ekp-layout-line-source-start line))
(+ base (ekp-layout-line-source-end line))
t)))
(defun ekp-gui-verify--scan-span (window span margin)
"Return measurement counters for SPAN in WINDOW using MARGIN."
(let* ((plan (ekp-buffer--span-plan span))
(records (ekp-buffer--span-lines span))
(target (ekp-buffer--span-width span))
(widest 0) (over 0) (wrong 0))
(dolist (record records)
(let* ((line (ekp-buffer--projected-line-line record))
(base (marker-position
(ekp-buffer--projected-line-base record)))
(pixel (ekp-gui-verify--line-width window base line)))
(setq widest (max widest pixel))
(when (> pixel (+ target margin))
(setq over (1+ over)))
(when (and (> (length (ekp-layout-line-gaps line)) 0)
(ekp-layout-line-break-kind line)
(> (abs (- pixel target)) margin))
(setq wrong (1+ wrong)))))
(list :widest widest :over over :wrong wrong
:lines (length records)
:static (and plan t))))
(defun ekp-gui-verify--scan (buffer &optional skip-predicate)
"Measure every projected display line of BUFFER.
When SKIP-PREDICATE is non-nil, omit spans for which it returns non-nil."
(with-current-buffer buffer
(let* ((window (get-buffer-window buffer))
(body (window-body-width window t))
(target ekp-buffer--auto-width)
(margin (max 2 ekp-buffer-margin-pixel))
(widest 0) (over 0) (wrong 0) (lines 0))
(dolist (span ekp-buffer--spans)
(unless (and skip-predicate (funcall skip-predicate span))
(let ((result (ekp-gui-verify--scan-span window span margin)))
(setq widest (max widest (plist-get result :widest))
over (+ over (plist-get result :over))
wrong (+ wrong (plist-get result :wrong))
lines (+ lines (plist-get result :lines))))))
(list :body body :target target :widest widest :over over
:wrong wrong :lines lines :exempt 0
:source-clean
(and (not (text-property-not-all
(point-min) (point-max) 'ekp-soft-break nil))
(not (text-property-not-all
(point-min) (point-max) 'ekp-glue nil))
(null (overlays-in (point-min) (point-max))))
:pass (and (= over 0) (= wrong 0) (> lines 0))))))
(defun ekp-gui-verify--atom-context ()
"Return the protected showcase atom and its projected plan context."
(let* ((atom-beg (text-property-any
(point-min) (point-max) 'ekp-no-break t))
(atom-end (and atom-beg
(next-single-property-change
atom-beg 'ekp-no-break nil (point-max))))
(span (and atom-beg
(seq-find
(lambda (candidate)
(and (<= (marker-position
(ekp-buffer--span-beg candidate))
atom-beg)
(< atom-beg
(marker-position
(ekp-buffer--span-end candidate)))))
ekp-buffer--spans))))
(unless (and atom-beg atom-end span)
(error "Showcase atom projection is unavailable"))
(list atom-beg atom-end span (ekp-buffer--span-plan span))))
(defun ekp-gui-verify-atom-adapter ()
"Return primitive evidence for the showcase rigid-atom regression."
(pcase-let* ((`(,atom-beg ,atom-end ,span ,plan)
(ekp-gui-verify--atom-context))
(base (marker-position (ekp-buffer--span-beg span)))
(atom-start (- atom-beg base))
(atom-finish (- atom-end base))
(prefix-end (1- atom-start))
(lines (ekp-layout-plan-lines plan))
(first (aref lines 0))
(atom-line
(seq-find
(lambda (line)
(and (<= (ekp-layout-line-source-start line) atom-start)
(>= (ekp-layout-line-source-end line) atom-finish)))
(append lines nil))))
`((width . ,ekp-showcase--width)
(first_line_end . ,(ekp-layout-line-source-end first))
(prefix_end . ,prefix-end)
(prefix_screen_lines
. ,(count-screen-lines base (+ base prefix-end)))
(atom_intact . ,(if atom-line t :false))
(source_exact
. ,(if (equal (buffer-substring-no-properties
(point-min) (point-max))
(substring-no-properties (ekp-showcase--sample)))
t :false))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(engine_c . ,(if (and ekp-use-c-module (ekp--c-available-p))
t :false))
(reflow_ms . ,(or ekp-showcase--last-ms 0)))))
(defun ekp-gui-verify-atom-assertions ()
"Return assertions for `ekp-gui-verify-atom-adapter'."
(let* ((adapter (ekp-gui-verify-atom-adapter))
(value (lambda (key) (cdr (assq key adapter)))))
`(((name . "full-prefix-before-atom")
(passed . ,(if (>= (funcall value 'first_line_end)
(funcall value 'prefix_end)) t :false)))
((name . "prefix-not-vertical")
(passed . ,(if (<= (funcall value 'prefix_screen_lines) 2)
t :false)))
((name . "atom-stays-on-one-plan-line")
(passed . ,(funcall value 'atom_intact)))
((name . "logical-source-exact")
(passed . ,(funcall value 'source_exact)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "c-engine-active")
(passed . ,(funcall value 'engine_c))))))
(defun ekp-gui-verify--source-text (beg end)
"Return source text from BEG through END without properties."
(buffer-substring-no-properties beg end))
(defun ekp-gui-verify--range-end (beg property)
"Return the end of PROPERTY's run starting at BEG."
(next-single-property-change beg property nil (point-max)))
(defun ekp-gui-verify--face-has-showcase-inline-role-p (face)
"Return non-nil when FACE carries the dedicated showcase inline role."
(cond
((memq face '(ekp-showcase-inline-code
ekp-showcase-inline-code-face))
t)
((consp face)
(seq-some #'ekp-gui-verify--face-has-showcase-inline-role-p face))))
(defun ekp-gui-verify--inline-role-at-p (pos)
"Return non-nil when POS is the automatic showcase inline role."
(or (get-text-property pos 'ekp-showcase-inline-code)
(ekp-gui-verify--face-has-showcase-inline-role-p
(get-text-property pos 'face))))
(defun ekp-gui-verify--face-code-range ()
"Return the automatic showcase inline-code role range."
(let ((pos (point-min))
hit)
(while (and (< pos (point-max)) (not hit))
(if (and (ekp-gui-verify--inline-role-at-p pos)
(not (get-text-property pos 'ekp-verbatim))
(not (get-text-property pos 'ekp-no-break)))
(setq hit
(cons pos
(min (or (next-single-property-change
pos 'ekp-showcase-inline-code
nil (point-max))
(point-max))
(ekp-gui-verify--range-end pos 'face)
(or (next-single-property-change
pos 'ekp-no-break nil (point-max))
(point-max))
(or (next-single-property-change
pos 'ekp-verbatim nil (point-max))
(point-max)))))
(setq pos (1+ pos))))
hit))
(defun ekp-gui-verify--projected-property-free-p (beg end)
"Return non-nil when BEG through END has no EKP projection props."
(seq-every-p
(lambda (property)
(not (text-property-not-all beg end property nil)))
'(ekp-justified ekp-buffer--display ekp-buffer--line-prefix
display line-prefix)))
(defun ekp-gui-verify--unplanned-span-count ()
"Return the number of installed spans that have no layout plan yet."
(let ((count 0))
(dolist (span ekp-buffer--spans count)
(unless (ekp-buffer--span-plan span)
(setq count (1+ count))))))
(defun ekp-gui-verify--current-live-prefix-p ()
"Return non-nil when the committed live prefix still matches the buffer."
(if-let* ((state ekp-buffer--live-state)
(beg (marker-position (ekp-buffer--live-state-beg state))))
(ekp-buffer--live-prefix-current-p
beg
(ekp-buffer--live-state-plan state)
(or (ekp-buffer--live-state-active-index state) 0)
(ekp-buffer--live-state-signatures state))
t))
(defun ekp-gui-verify--unplanned-span-counts ()
"Return raw, live, and stale nil-plan span counts."
(let ((raw 0)
(live 0)
(stale 0)
(live-spans (and ekp-buffer--live-state
(ekp-buffer--live-state-spans
ekp-buffer--live-state)))
(live-current (ekp-gui-verify--current-live-prefix-p)))
(dolist (span ekp-buffer--spans)
(unless (ekp-buffer--span-plan span)
(setq raw (1+ raw))
(if (and live-current (memq span live-spans))
(setq live (1+ live))
(setq stale (1+ stale)))))
`((raw . ,raw)
(live . ,live)
(stale . ,stale)
(live_state_current . ,(if live-current t :false)))))
(defun ekp-gui-verify--isolated-cjk-cascades ()
"Return runs of three or more consecutive isolated CJK source lines."
(let (hits)
(dolist (span ekp-buffer--spans)
(when-let* ((plan (ekp-buffer--span-plan span))
(source (ekp-layout-plan-string plan)))
(let (run)
(dolist (line (append (ekp-layout-plan-lines plan) nil))
(if (ekp-gui-verify--isolated-cjk-line-p line source)
(push (substring source
(ekp-layout-line-source-start line)
(ekp-layout-line-source-end line))
run)
(when (>= (length run) 3)
(push (nreverse run) hits))
(setq run nil)))
(when (>= (length run) 3)
(push (nreverse run) hits)))))
(nreverse hits)))
(defun ekp-gui-verify--hard-atom-range ()
"Return the explicit hard no-break range in the showcase."
(when-let* ((beg (text-property-any
(point-min) (point-max) 'ekp-no-break t)))
(cons beg (ekp-gui-verify--range-end beg 'ekp-no-break))))
(defun ekp-gui-verify--verbatim-range ()
"Return the explicit verbatim block range in the showcase."
(when-let* ((beg (text-property-any
(point-min) (point-max) 'ekp-verbatim t)))
(cons beg (ekp-gui-verify--range-end beg 'ekp-verbatim))))
(defun ekp-gui-verify--span-for-position (position)
"Return the projected span containing POSITION."
(seq-find
(lambda (span)
(and (<= (marker-position (ekp-buffer--span-beg span)) position)
(< position (marker-position (ekp-buffer--span-end span)))))
ekp-buffer--spans))
(defun ekp-gui-verify--range-lines (range)
"Return layout lines intersecting RANGE."
(when-let* ((span (and range
(ekp-gui-verify--span-for-position (car range))))
(plan (ekp-buffer--span-plan span)))
(let* ((base (marker-position (ekp-buffer--span-beg span)))
(start (- (car range) base))
(finish (- (cdr range) base)))
(seq-filter
(lambda (line)
(and (< (ekp-layout-line-source-start line) finish)
(< start (ekp-layout-line-source-end line))))
(append (ekp-layout-plan-lines plan) nil)))))
(defun ekp-gui-verify--range-plan-complete-p (range)
"Return non-nil when RANGE's plan is nonempty and covers its source."
(when-let* ((span (and range
(ekp-gui-verify--span-for-position (car range))))
(plan (ekp-buffer--span-plan span))
(lines (append (ekp-layout-plan-lines plan) nil)))
(and (= (ekp-layout-line-source-start (car lines)) 0)
(= (ekp-layout-line-source-end (car (last lines)))
(length (ekp-layout-plan-string plan))))))
(defun ekp-gui-verify--range-intact-p (range)
"Return non-nil when RANGE is contained in one planned line."
(seq-some
(lambda (line)
(let* ((span (ekp-gui-verify--span-for-position (car range)))
(base (marker-position (ekp-buffer--span-beg span)))
(start (- (car range) base))
(finish (- (cdr range) base)))
(and (<= (ekp-layout-line-source-start line) start)
(>= (ekp-layout-line-source-end line) finish))))
(ekp-gui-verify--range-lines range)))
(defun ekp-gui-verify--range-break-text (range line)
"Return LINE's break source inside RANGE, or nil when outside."
(let ((start (ekp-layout-line-break-source-start line))
(end (ekp-layout-line-break-source-end line)))
(when-let* ((span (and start end (< start end)
(ekp-gui-verify--span-for-position
(car range)))))
(let* ((base (marker-position (ekp-buffer--span-beg span)))
(range-start (- (car range) base))
(range-end (- (cdr range) base)))
(when (and (<= range-start start) (<= end range-end))
(substring (ekp-gui-verify--source-text (car range) (cdr range))
(- start range-start)
(- end range-start)))))))
(defun ekp-gui-verify--inline-legal-breaks-p (range)
"Return non-nil when RANGE uses only source-space breaks."
(and range
(seq-every-p
(lambda (line)
(or (not (ekp-layout-line-break-kind line))
(let ((break-text
(ekp-gui-verify--range-break-text range line)))
(or (not break-text)
(string-match-p
"\\`[[:space:]\n\r\t]+\\'" break-text)))))
(ekp-gui-verify--range-lines range))))
(defun ekp-gui-verify--inline-hyphenated-p (range)
"Return non-nil when RANGE has a discretionary hyphenated line."
(seq-some #'ekp-layout-line-hyphen-p
(or (ekp-gui-verify--range-lines range) nil)))
(defun ekp-gui-verify--inline-line-evidence (range)
"Return planned line evidence for inline RANGE."
(let* ((span (and range (ekp-gui-verify--span-for-position (car range))))
(plan (and span (ekp-buffer--span-plan span)))
(source (and plan (ekp-layout-plan-string plan)))
(base (and span (marker-position (ekp-buffer--span-beg span))))
(range-start (and base (- (car range) base)))
(range-end (and base (- (cdr range) base)))
(lines (or (ekp-gui-verify--range-lines range) nil))
(line-count (length lines))
break-map internal-breaks)
(dolist (line lines)
(let* ((start (ekp-layout-line-break-source-start line))
(end (ekp-layout-line-break-source-end line))
(kind (ekp-layout-line-break-kind line))
(internal (and source start end (< start end)
range-start range-end
(< range-start start)
(<= end range-end)))
(source-space
(and source start end (< start end)
(string-match-p
"\\`[[:space:]\n\r\t]+\\'"
(substring source start end))))
(hyphen (and internal (ekp-layout-line-hyphen-p line)))
(entry
`((start . ,(or start 0))
(end . ,(or end 0))
(kind . ,(ekp-gui-verify--json-policy-value kind))
(internal . ,(if internal t :false))
(source_space . ,(if source-space t :false))
(hyphen . ,(if hyphen t :false)))))
(when (or kind start end)
(push entry break-map))
(when internal
(push entry internal-breaks))))
`((automatic_inline_line_count . ,line-count)
(automatic_inline_wrapped . ,(if (> line-count 1) t :false))
(automatic_inline_breaks_source_spaces
. ,(if (and (> line-count 1)
internal-breaks
(seq-every-p
(lambda (entry)
(and (eq (cdr (assq 'source_space entry)) t)
(eq (cdr (assq 'hyphen entry)) :false)))
internal-breaks))
t :false))
(automatic_inline_break_map . ,(nreverse break-map)))))
(defun ekp-gui-verify--isolated-cjk-line-p (line source)
"Return non-nil when LINE projects a single CJK character from SOURCE."
(let ((text (string-trim
(substring source
(ekp-layout-line-source-start line)
(ekp-layout-line-source-end line)))))
(and (= (length text) 1)
(let ((char (aref text 0)))
(and (<= #x4E00 char) (<= char #x9FFF))))))
(defun ekp-gui-verify--isolated-cjk-lines ()
"Return isolated CJK planned lines in projected showcase spans."
(let (hits)
(dolist (span ekp-buffer--spans)
(when-let* ((plan (ekp-buffer--span-plan span))
(source (ekp-layout-plan-string plan)))
(dolist (line (append (ekp-layout-plan-lines plan) nil))
(when (ekp-gui-verify--isolated-cjk-line-p line source)
(push (substring source
(ekp-layout-line-source-start line)
(ekp-layout-line-source-end line))
hits)))))
(nreverse hits)))
(defun ekp-gui-verify--refresh-showcase-width (width)
"Open the showcase and refresh it at WIDTH."
(when (get-buffer "*ekp-showcase*")
(kill-buffer "*ekp-showcase*"))
(ekp-showcase)
(with-current-buffer "*ekp-showcase*"
(ekp-showcase-set-width width)
(redisplay t)
(current-buffer)))
(defun ekp-gui-verify--showcase-policy-evidence ()
"Return G004 policy evidence for the current showcase buffer."
(let* ((auto (ekp-gui-verify--face-code-range))
(manual (ekp-gui-verify--hard-atom-range))
(block (ekp-gui-verify--verbatim-range))
(auto-lines (ekp-gui-verify--inline-line-evidence auto))
(sample (substring-no-properties (ekp-showcase--sample))))
`((width . ,ekp-showcase--width)
(automatic_inline_present . ,(if auto t :false))
(automatic_inline_plan_complete
. ,(if (ekp-gui-verify--range-plan-complete-p auto) t :false))
(automatic_inline_explicit_no_break
. ,(if (and auto (text-property-not-all
(car auto) (cdr auto) 'ekp-no-break nil))
t :false))
(automatic_inline_literal_source
. ,(if (and auto
(string-match-p "[[:space:]][^[:space:]]+[[:space:]]"
(ekp-gui-verify--source-text
(car auto) (cdr auto))))
t :false))
(automatic_inline_legal_breaks
. ,(if (and auto
(ekp-gui-verify--inline-legal-breaks-p auto))
t :false))
(automatic_inline_no_hyphen
. ,(if (and auto
(not (ekp-gui-verify--inline-hyphenated-p auto)))
t :false))
,@auto-lines
(manual_hard_atom_present . ,(if manual t :false))
(manual_hard_atom_distinct
. ,(if (and auto manual (not (equal auto manual))) t :false))
(manual_hard_atom_explicit
. ,(if (and manual
(not (text-property-not-all
(car manual) (cdr manual) 'ekp-no-break t)))
t :false))
(manual_hard_atom_intact
. ,(if (and manual
(ekp-gui-verify--range-intact-p manual))
t :false))
(block_verbatim_present . ,(if block t :false))
(block_verbatim_intact
. ,(if (and block
(ekp-gui-verify--projected-property-free-p
(car block) (cdr block)))
t :false))
(isolated_cjk_cascades . ,(ekp-gui-verify--isolated-cjk-cascades))
(isolated_cjk_lines . ,(ekp-gui-verify--isolated-cjk-lines))
(source_exact
. ,(if (equal (buffer-substring-no-properties
(point-min) (point-max))
sample)
t :false))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(engine_c . ,(if (and ekp-use-c-module (ekp--c-available-p))
t :false)))))
(defun ekp-gui-verify-showcase-policy-adapter (&optional width)
"Return G004 machine evidence for the showcase policy contract."
(with-current-buffer (ekp-gui-verify--refresh-showcase-width
(or width 280))
(ekp-gui-verify--showcase-policy-evidence)))
(defun ekp-gui-verify-showcase-parshape-policy-adapter (&optional width)
"Return showcase policy evidence with parshape enabled at WIDTH."
(with-current-buffer (ekp-gui-verify--refresh-showcase-width
(or width 280))
(setq-local ekp-showcase--parshape-on t)
(ekp-showcase--refresh)
(redisplay t)
(ekp-gui-verify--showcase-policy-evidence)))
(defun ekp-gui-verify-showcase-policy-assertions ()
"Return G004 assertions for `ekp-gui-verify-showcase-policy-adapter'."
(let* ((adapter (ekp-gui-verify-showcase-policy-adapter 280))
(value (lambda (key) (cdr (assq key adapter)))))
`(((name . "automatic-inline-face-present")
(passed . ,(funcall value 'automatic_inline_present)))
((name . "automatic-inline-plan-complete")
(passed . ,(funcall value 'automatic_inline_plan_complete)))
((name . "automatic-inline-has-no-explicit-no-break")
(passed . ,(if (eq (funcall value
'automatic_inline_explicit_no_break)
:false)
t :false)))
((name . "automatic-inline-keeps-literal-source")
(passed . ,(funcall value 'automatic_inline_literal_source)))
((name . "automatic-inline-breaks-only-legally")
(passed . ,(funcall value 'automatic_inline_legal_breaks)))
((name . "automatic-inline-never-discretionary-hyphenates")
(passed . ,(funcall value 'automatic_inline_no_hyphen)))
((name . "manual-hard-atom-is-distinct")
(passed . ,(funcall value 'manual_hard_atom_distinct)))
((name . "manual-hard-atom-remains-explicit")
(passed . ,(funcall value 'manual_hard_atom_explicit)))
((name . "manual-hard-atom-remains-intact")
(passed . ,(funcall value 'manual_hard_atom_intact)))
((name . "block-code-stays-verbatim")
(passed . ,(if (and (funcall value 'block_verbatim_present)
(funcall value 'block_verbatim_intact))
t :false)))
((name . "narrow-rendering-has-no-isolated-cjk-cascade")
(passed . ,(if (null (funcall value 'isolated_cjk_cascades))
t :false)))
((name . "narrow-rendering-has-no-isolated-cjk-lines")
(passed . ,(if (null (funcall value 'isolated_cjk_lines))
t :false)))
((name . "logical-source-exact")
(passed . ,(funcall value 'source_exact)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "c-engine-active")
(passed . ,(funcall value 'engine_c))))))
(ert-deftest ekp-gui-verify-g004-showcase-policy-contract ()
"The showcase separates automatic inline code from manual hard atoms."
(let ((assertions (ekp-gui-verify-showcase-policy-assertions)))
(dolist (assertion assertions)
(ert-info ((cdr (assq 'name assertion)))
(should (eq (cdr (assq 'passed assertion)) t))))))
(ert-deftest ekp-gui-verify-showcase-1d-rejects-isolated-cjk-lines ()
"The default 168px showcase oracle rejects isolated CJK rows."
(let* ((adapter (ekp-gui-verify-showcase-policy-adapter 168))
(isolated (cdr (assq 'isolated_cjk_lines adapter))))
(should (eq (cdr (assq 'automatic_inline_plan_complete adapter)) t))
(should-not isolated)))
(ert-deftest ekp-gui-verify-showcase-parshape-rejects-isolated-cjk-lines ()
"The parshape-on 280px showcase oracle rejects isolated CJK rows."
(let* ((adapter (ekp-gui-verify-showcase-parshape-policy-adapter 280))
(isolated (cdr (assq 'isolated_cjk_lines adapter))))
(should (eq (cdr (assq 'automatic_inline_plan_complete adapter)) t))
(should-not isolated)))
;;;###autoload
(defun ekp-gui-verify ()
"Verify pixel-exact justification against this session's display.
Opens the ekp showcase, enables follow-window justification, and
checks with `window-text-pixel-size' that every justified line
renders at exactly the window's text width — under YOUR fonts,
themes, remappings and text-scale. Reports PASS or FAIL."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(ekp-auto-justify-mode 1)
(when (timerp ekp-buffer--resize-timer)
(cancel-timer ekp-buffer--resize-timer))
(ekp-buffer--reflow (current-buffer) (ekp-buffer--effective-width))
(redisplay t)
(let* ((r (ekp-gui-verify--scan (current-buffer)))
(msg (format
"ekp-gui-verify: %s — %d lines, widest %dpx vs target %dpx (window %dpx)%s"
(if (plist-get r :pass) "PASS" "FAIL")
(plist-get r :lines) (plist-get r :widest)
(plist-get r :target) (plist-get r :body)
(if (> (plist-get r :exempt) 0)
(format ", %d verbatim lines exempt"
(plist-get r :exempt))
""))))
(message "%s" msg)
r)))
(defun ekp-gui-verify--case (name setup)
"Run one matrix case NAME with buffer SETUP; return its result plist."
;; Leftover debounce timers from the previous case must not fire
;; into this case's fresh buffer.
(dolist (fn (list #'ekp-buffer--reflow
#'ekp-buffer--process-chunk))
(cancel-function-timers fn))
(when (get-buffer "*ekp-showcase*")
(kill-buffer "*ekp-showcase*"))
(ekp-showcase)
(redisplay t)
(with-current-buffer "*ekp-showcase*"
(funcall setup)
(redisplay t)
(ekp-auto-justify-mode 1)
(when (timerp ekp-buffer--resize-timer)
(cancel-timer ekp-buffer--resize-timer))
(ekp-buffer--reflow (current-buffer) (ekp-buffer--effective-width))
(redisplay t)
(let ((r (ekp-gui-verify--scan (current-buffer))))
(prog1 (append (list :name name) r)
(ekp-auto-justify-mode -1)))))
(defun ekp-gui-verify--format-result (result)
"Format one matrix RESULT plist as a report line."
(format "%-22s body=%4d target=%4d widest=%4d over=%d/%d %s"
(plist-get result :name)
(plist-get result :body)
(plist-get result :target)
(plist-get result :widest)
(plist-get result :over)
(plist-get result :lines)
(if (plist-get result :pass) "PASS" "FAIL")))
(defun ekp-gui-verify--report (results)
"Report matrix RESULTS and return their formatted table.
In batch mode, terminate with status 1 when any result fails."
(let ((table (mapconcat #'ekp-gui-verify--format-result results "\n"))
(passed t))
(dolist (result results)
(unless (plist-get result :pass)
(setq passed nil)))
(if noninteractive
(princ (concat table "\n"))
(with-current-buffer (get-buffer-create "*ekp-gui-verify*")
(erase-buffer)
(insert table "\n")
(display-buffer (current-buffer))))
(when (and noninteractive (not passed))
(kill-emacs 1))
table))
(defun ekp-gui-verify--pixel-width (window beg end)
"Return displayed width from BEG to END in WINDOW."
(car (window-text-pixel-size window beg end t)))
(defun ekp-gui-verify--exact-ascii-glue (window)
"Return failed 164px ASCII glue targets in WINDOW."
(let ((natural (ekp-gui-verify--pixel-width window 2 3))
failures)
(dotimes (index 64)
(let ((target (1+ index)))
(with-silent-modifications
(ekp-buffer--remove-properties 1 4)
(ekp-buffer--put-display
2 3 (ekp-buffer--space-display natural target)))
(redisplay t)
(let ((actual (- (ekp-gui-verify--pixel-width window 1 4)
(ekp-gui-verify--pixel-width window 1 2)
(ekp-gui-verify--pixel-width window 3 4))))
(unless (= actual target)
(push (cons target actual) failures)))))
(nreverse failures)))
(defun ekp-gui-verify--exact-cjk-glue (window)
"Return failed 164px zero-source CJK glue targets in WINDOW."
(let ((first (ekp-gui-verify--pixel-width window 1 2))
(second (ekp-gui-verify--pixel-width window 2 3))
failures)
(dotimes (index 64)
(let ((target (1+ index)))
(with-silent-modifications
(ekp-buffer--remove-properties 1 3)
(ekp-buffer--put-display
1 2 (ekp-buffer--min-width (+ first target))))
(redisplay t)
(let ((actual (- (ekp-gui-verify--pixel-width window 1 3)
first second)))
(unless (= actual target)
(push (cons target actual) failures)))))
(nreverse failures)))
(defun ekp-gui-verify--hyphen-width (text)
"Return a width that chooses a discretionary hyphen in TEXT."
(seq-find
(lambda (width)
(seq-some
(lambda (line) (ekp-layout-line-hyphen-p line))
(append (ekp-layout-plan-lines
(ekp-layout-plan text width))
nil)))
(number-sequence 60 220 4)))
(defun ekp-gui-verify--hyphen-owner ()
"Return the source position owning the displayed discretionary hyphen."
(seq-find
(lambda (position)
(let ((display (get-text-property position 'display)))
(and (stringp display)
(string-match-p "-\n"
(substring-no-properties display)))))
(number-sequence (point-min) (1- (point-max)))))
(defun ekp-gui-verify--editor-semantics (window owner text)
"Verify point, region, and mouse semantics in WINDOW at OWNER for TEXT."
(let ((point-visible
(cl-loop
for position from (point-min) to (point-max)
always
(progn
(goto-char position)
(redisplay t)
(pos-visible-in-window-p position window t)))))
(goto-char (point-min))
(let ((vertical (vertical-motion 1 window)))
(set-mark (point-min))
(goto-char (point-max))
(activate-mark)
(redisplay t)
(let* ((xy (pos-visible-in-window-p owner window t))
(posn (and xy
(posn-at-x-y (car xy) (cadr xy) window)))
(mouse (and posn (posn-point posn))))
(list
:point-visible point-visible
:vertical-motion vertical
:vertical-source-position (point)
:region-source
(and (use-region-p)
(equal (buffer-substring-no-properties
(region-beginning) (region-end))
text))
:mouse-source-position
(and (integer-or-marker-p mouse)
(<= (point-min) mouse (point-max))))))))
(defun ekp-gui-verify--hyphen-case (window text)
"Return verification data for display-only hyphenation of TEXT in WINDOW."
(let ((width (ekp-gui-verify--hyphen-width text)))
(ekp-justify-region (point-min) (point-max) width)
(redisplay t)
(let ((owner (ekp-gui-verify--hyphen-owner)))
(append
(list
:width width
:source-clean
(equal (substring-no-properties (buffer-string)) text)
:source-lines (cl-count ?\n (buffer-string))
:screen-lines (count-screen-lines (point-min) (point-max))
:hyphen-display (and owner t)
:overlays (length (overlays-in (point-min) (point-max)))
:scan (ekp-gui-verify--scan (current-buffer))
:window-width (window-body-width window t))
(ekp-gui-verify--editor-semantics window owner text)))))
(defun ekp-gui-verify--display-properties-pass-p (result)
"Return non-nil when display-property verification RESULT passes."
(let ((hyphen (plist-get result :hyphen)))
(and (null (plist-get result :ascii-failures))
(null (plist-get result :cjk-failures))
(plist-get hyphen :source-clean)
(= (plist-get hyphen :source-lines) 0)
(> (plist-get hyphen :screen-lines) 1)
(plist-get hyphen :hyphen-display)
(= (plist-get hyphen :overlays) 0)
(plist-get hyphen :point-visible)
(> (plist-get hyphen :vertical-motion) 0)
(plist-get hyphen :region-source)
(plist-get hyphen :mouse-source-position)
(plist-get (plist-get hyphen :scan) :pass))))
;;;###autoload
(defun ekp-gui-verify-display-properties ()
"Verify exact text-property glue and display-only hyphenation."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(let ((buffer (generate-new-buffer "*ekp-display-properties*"))
result)
(unwind-protect
(progn
(switch-to-buffer buffer)
(delete-other-windows)
(insert "a b")
(redisplay t)
(let ((ascii (ekp-gui-verify--exact-ascii-glue
(selected-window))))
(erase-buffer)
(insert "中文")
(redisplay t)
(let ((cjk (ekp-gui-verify--exact-cjk-glue
(selected-window))))
(erase-buffer)
(let ((text
"extraordinary hyphenation demonstration paragraph"))
(insert text)
(setq result
(list :ascii-failures ascii
:cjk-failures cjk
:hyphen
(ekp-gui-verify--hyphen-case
(selected-window) text)))
(setq result
(plist-put
result :pass
(ekp-gui-verify--display-properties-pass-p
result)))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))
(when (called-interactively-p 'interactive)
(message "ekp display-property verification: %s"
(if (plist-get result :pass) "PASS" "FAIL")))
result))
(defvar-local ekp-gui-verify--live-original nil)
(defvar-local ekp-gui-verify--live-expected nil)
(defvar-local ekp-gui-verify--live-suffix " overflow")
(defvar-local ekp-gui-verify--live-yank " pasted 中文 semantic prefix")
(defvar-local ekp-gui-verify--live-original-signatures nil)
(defvar-local ekp-gui-verify--live-original-projection-hash nil)
(defvar-local ekp-gui-verify--live-original-active-index nil)
(defvar-local ekp-gui-verify--live-original-prefix-end nil)
(defvar-local ekp-gui-verify--live-original-width nil)
(defvar-local ekp-gui-verify--live-original-screen-lines nil)
(defvar-local ekp-gui-verify--live-post-crossing-state nil)
(defvar-local ekp-gui-verify--live-motion-generation nil)
(defvar-local ekp-gui-verify--live-motion-cache-size nil)
(defvar-local ekp-gui-verify--live-motion-state nil)
(defvar-local ekp-gui-verify--live-middle-position nil)
(defvar-local ekp-gui-verify--live-middle-later-beg nil)
(defvar-local ekp-gui-verify--live-middle-later-end nil)
(defvar-local ekp-gui-verify--live-middle-later-hash nil)
(defvar-local ekp-gui-verify--live-middle-plan nil)
(defvar-local ekp-gui-verify--live-middle-spans nil)
(defvar-local ekp-gui-verify--live-stage "setup")
(defvar-local ekp-gui-verify--showcase-temporal-source nil)
(defvar-local ekp-gui-verify--showcase-temporal-before-generation nil)
(defvar-local ekp-gui-verify--showcase-temporal-before-projection nil)
(defun ekp-gui-verify--timer-state (timer)
"Return a JSON-compatible state for TIMER."
(if (timerp timer) t :false))
(defun ekp-gui-verify--current-source-hash ()
"Return the current buffer's logical source hash."
(secure-hash 'sha256
(buffer-substring-no-properties
(point-min) (point-max))))
(defun ekp-gui-verify--json-policy-value (value)
"Return VALUE as a JSON-compatible policy primitive."
(if (symbolp value)
(and value (symbol-name value))
value))
(defun ekp-gui-verify--current-policy-context ()
"Return a compact JSON-compatible policy context summary."
(let ((context (ekp-buffer--policy-context ekp-buffer--auto-width)))
`((inline_code . ,(ekp-gui-verify--json-policy-value
(plist-get context :inline-code-policy)))
(hyphenation . ,(ekp-gui-verify--json-policy-value
(plist-get context :hyphenation)))
(kinsoku . ,(ekp-gui-verify--json-policy-value
(plist-get context :kinsoku-profile)))
(overlong . ,(ekp-gui-verify--json-policy-value
(plist-get context :overlong-token-policy)))
(width . ,(or (plist-get context :width) 0)))))
(defun ekp-gui-verify-showcase-temporal-adapter
(&optional stage completed)
"Return current G004 temporal showcase state for STAGE."
(let* ((policy (ekp-gui-verify--showcase-policy-evidence))
(source-hash (ekp-gui-verify--current-source-hash))
(unplanned (ekp-gui-verify--unplanned-span-counts))
(raw-unplanned (cdr (assq 'raw unplanned)))
(live-unplanned (cdr (assq 'live unplanned)))
(stale-unplanned (cdr (assq 'stale unplanned)))
(live-state-current (cdr (assq 'live_state_current unplanned)))
(generation-delta
(and ekp-gui-verify--showcase-temporal-before-generation
(- ekp-buffer--generation
ekp-gui-verify--showcase-temporal-before-generation))))
`((stage . ,(or stage "current"))
(width . ,ekp-showcase--width)
(selected_target_window
. ,(if (and (= (length (seq-remove #'window-minibuffer-p
(window-list)))
1)
(eq (window-buffer (selected-window))
(current-buffer)))
t :false))
(source_hash . ,source-hash)
(source_unchanged
. ,(if (or (not ekp-gui-verify--showcase-temporal-source)
(equal source-hash
ekp-gui-verify--showcase-temporal-source))
t :false))
(projection_hash . ,(ekp-gui-verify--projection-hash))
(projection_published . ,(if ekp-buffer--spans t :false))
(current_projection
. ,(if (and ekp-buffer--spans
(= stale-unplanned 0)
(eq live-state-current t))
t :false))
(projection_changed
. ,(if (and ekp-gui-verify--showcase-temporal-before-projection
(not (equal (ekp-gui-verify--projection-hash)
ekp-gui-verify--showcase-temporal-before-projection)))
t :false))
(generation . ,ekp-buffer--generation)
(generation_delta . ,(or generation-delta 0))
(resize_timer . ,(ekp-gui-verify--timer-state
ekp-buffer--resize-timer))
(policy_timer . ,(ekp-gui-verify--timer-state
ekp-buffer--policy-reflow-timer))
(unplanned_spans . ,raw-unplanned)
(live_unplanned_spans . ,live-unplanned)
(stale_unplanned_spans . ,stale-unplanned)
(live_state_current . ,live-state-current)
(timers_settled
. ,(if (and (not (timerp ekp-buffer--resize-timer))
(not (timerp ekp-buffer--policy-reflow-timer)))
t :false))
(pending_chunks . ,(if ekp-buffer--pending t :false))
(overlays . ,(cdr (assq 'overlays policy)))
(engine_c . ,(cdr (assq 'engine_c policy)))
(automatic_inline_plan_complete
. ,(cdr (assq 'automatic_inline_plan_complete policy)))
(automatic_inline_line_count
. ,(cdr (assq 'automatic_inline_line_count policy)))
(automatic_inline_wrapped
. ,(cdr (assq 'automatic_inline_wrapped policy)))
(automatic_inline_breaks_source_spaces
. ,(cdr (assq 'automatic_inline_breaks_source_spaces policy)))
(automatic_inline_break_map
. ,(cdr (assq 'automatic_inline_break_map policy)))
(isolated_cjk_cascades
. ,(cdr (assq 'isolated_cjk_cascades policy)))
(isolated_cjk_lines
. ,(cdr (assq 'isolated_cjk_lines policy)))
(policy . ,(ekp-gui-verify--current-policy-context))
(completed . ,(if completed t :false)))))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-setup ()
"Create a clean single-window showcase temporal verification buffer."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(cancel-function-timers #'ekp-buffer--reflow)
(cancel-function-timers #'ekp-buffer--process-chunk)
(cancel-function-timers #'ekp-buffer--reflow-for-policy-change)
(when (get-buffer "*ekp-showcase*")
(kill-buffer "*ekp-showcase*"))
(delete-other-windows)
(ekp-showcase)
(delete-other-windows)
(with-current-buffer "*ekp-showcase*"
(when ekp-auto-justify-mode
(ekp-auto-justify-mode -1))
(setq ekp-gui-verify--showcase-temporal-source nil
ekp-gui-verify--showcase-temporal-before-generation nil
ekp-gui-verify--showcase-temporal-before-projection nil)
(kill-local-variable 'ekp-inline-code-policy)
(ekp-showcase-set-width 480)
(redisplay t)
(ekp-gui-verify-showcase-temporal-adapter "setup" nil)))
(defun ekp-gui-verify--temporal-set-width (width stage)
"Set showcase WIDTH and record temporal STAGE."
(with-current-buffer "*ekp-showcase*"
(ekp-showcase-set-width width)
(redisplay t)
(ekp-gui-verify-showcase-temporal-adapter stage nil)))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-width-480 ()
"Record manual showcase width 480 in the temporal scenario."
(interactive)
(ekp-gui-verify--temporal-set-width 480 "width-480"))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-width-280 ()
"Record manual showcase width 280 in the temporal scenario."
(interactive)
(ekp-gui-verify--temporal-set-width 280 "width-280"))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-width-340 ()
"Record manual showcase width 340 in the temporal scenario."
(interactive)
(ekp-gui-verify--temporal-set-width 340 "width-340"))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-policy-before ()
"Enable auto layout and capture the policy-change before checkpoint."
(interactive)
(with-current-buffer "*ekp-showcase*"
(ekp-auto-justify-mode 1)
(let ((deadline (+ (float-time) 2.0)))
(while (and (or (timerp ekp-buffer--resize-timer)
ekp-buffer--pending)
(< (float-time) deadline))
(sit-for 0.05)))
(setq ekp-gui-verify--showcase-temporal-source
(ekp-gui-verify--current-source-hash)
ekp-gui-verify--showcase-temporal-before-generation
ekp-buffer--generation
ekp-gui-verify--showcase-temporal-before-projection
(ekp-gui-verify--projection-hash))
(redisplay t)
(ekp-gui-verify-showcase-temporal-adapter "policy-before" nil)))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-policy-immediate ()
"Apply a local inline-code policy change and capture pending timer state."
(interactive)
(with-current-buffer "*ekp-showcase*"
(setq-local ekp-inline-code-policy 'normal)
(ekp-gui-verify-showcase-temporal-adapter "policy-immediate" nil)))
;;;###autoload
(defun ekp-gui-verify-showcase-temporal-policy-settled ()
"Wait for the real policy timer and capture the settled checkpoint."
(interactive)
(with-current-buffer "*ekp-showcase*"
(let ((deadline (+ (float-time) 2.0)))
(while (and (or (timerp ekp-buffer--policy-reflow-timer)
(timerp ekp-buffer--resize-timer)
ekp-buffer--pending)
(< (float-time) deadline))
(sit-for 0.05)))
(redisplay t)
(ekp-gui-verify-showcase-temporal-adapter "policy-settled" t)))
(defun ekp-gui-verify-showcase-temporal-assertions (state)
"Return assertions for temporal showcase adapter STATE."
(let* ((stage (cdr (assq 'stage state)))
(immediate-p (equal stage "policy-immediate"))
(manual-wrap-stage-p (member stage '("width-280" "width-340")))
(completed-p (eq (cdr (assq 'completed state)) t))
(policy-timer (cdr (assq 'policy_timer state)))
(unplanned-spans (cdr (assq 'unplanned_spans state)))
(stale-unplanned-spans
(cdr (assq 'stale_unplanned_spans state)))
(generation-delta (cdr (assq 'generation_delta state)))
(immediate-observed-p
(or (eq policy-timer t)
(and (eq policy-timer :false)
(= (or generation-delta 0) 1))))
(policy (cdr (assq 'policy state))))
`(((name . "policy-timer-state-valid")
(passed . ,(if (memq policy-timer '(t :false))
t :false)))
((name . "automatic-inline-plan-complete")
(passed . ,(if (eq (cdr (assq 'automatic_inline_plan_complete state)) t)
t :false)))
((name . "immediate-policy-timer-pending")
(passed . ,(if (or (not immediate-p)
immediate-observed-p)
t :false)))
((name . "immediate-unplanned-spans-only-while-policy-pending")
(passed . ,(if (or (not immediate-p)
(= (or unplanned-spans 0) 0)
immediate-observed-p)
t :false)))
((name . "manual-width-automatic-inline-wrapped")
(passed . ,(if (or (not manual-wrap-stage-p)
(eq (cdr (assq 'automatic_inline_wrapped state)) t))
t :false)))
((name . "manual-width-inline-breaks-use-source-spaces")
(passed . ,(if (or (not manual-wrap-stage-p)
(eq (cdr (assq 'automatic_inline_breaks_source_spaces
state))
t))
t :false)))
((name . "settled-policy-timer-cleared")
(passed . ,(if (or (not completed-p)
(eq policy-timer :false))
t :false)))
((name . "settled-resize-timer-cleared")
(passed . ,(if (or (not completed-p)
(eq (cdr (assq 'resize_timer state)) :false))
t :false)))
((name . "settled-no-stale-resize-or-policy-timers")
(passed . ,(if (or (not completed-p)
(eq (cdr (assq 'timers_settled state)) t))
t :false)))
((name . "settled-exactly-one-generation-reflow")
(passed . ,(if (or (not completed-p)
(= (or generation-delta 0) 1))
t :false)))
((name . "settled-source-hash-unchanged")
(passed . ,(if (or (not completed-p)
(eq (cdr (assq 'source_unchanged state)) t))
t :false)))
((name . "settled-effective-inline-policy-normal")
(passed . ,(if (or (not completed-p)
(equal (cdr (assq 'inline_code policy)) "normal"))
t :false)))
((name . "settled-zero-stale-unplanned-spans")
(passed . ,(if (or (not completed-p)
(and (numberp stale-unplanned-spans)
(= stale-unplanned-spans 0)))
t :false)))
((name . "settled-current-projection")
(passed . ,(if (or (not completed-p)
(eq (cdr (assq 'current_projection state)) t))
t :false)))
((name . "settled-zero-overlays")
(passed . ,(if (= (cdr (assq 'overlays state)) 0) t :false)))
((name . "settled-projection-published")
(passed . ,(if (eq (cdr (assq 'projection_published state)) t)
t :false)))
((name . "settled-c-engine-active")
(passed . ,(if (eq (cdr (assq 'engine_c state)) t)
t :false)))
((name . "settled-no-isolated-cjk-cascade")
(passed . ,(if (null (cdr (assq 'isolated_cjk_cascades state)))
t :false)))
((name . "settled-no-isolated-cjk-lines")
(passed . ,(if (null (cdr (assq 'isolated_cjk_lines state)))
t :false)))
((name . "settled-one-selected-target-window")
(passed . ,(if (eq (cdr (assq 'selected_target_window state)) t)
t :false)))
((name . "settled-no-pending-chunks")
(passed . ,(if (or (not completed-p)
(eq (cdr (assq 'pending_chunks state)) :false))
t :false))))))
(ert-deftest ekp-gui-verify-g004-temporal-skips-unplanned-spans ()
"Treat pending nil-plan spans as observable state, not verifier failure."
(with-temp-buffer
(insert "中文 abc")
(let ((ekp-buffer--spans
(list (ekp-buffer--span-create
:beg (copy-marker (point-min))
:end (copy-marker (point-max))
:width 10
:plan nil
:lines nil))))
(should (= (ekp-gui-verify--unplanned-span-count) 1))
(should (null (ekp-gui-verify--isolated-cjk-cascades)))
(should (null (ekp-gui-verify--isolated-cjk-lines)))
(should (null (ekp-gui-verify--range-lines
(cons (point-min) (point-max))))))))
(defun ekp-gui-verify--json-primitive-tree-p (value)
"Return non-nil when VALUE contains only JSON-compatible primitives."
(cond
((or (null value) (eq value t) (eq value :false)
(stringp value) (numberp value))
t)
((and (consp value) (symbolp (car value)))
(and (symbolp (car value))
(ekp-gui-verify--json-primitive-tree-p (cdr value))))
((listp value)
(seq-every-p #'ekp-gui-verify--json-primitive-tree-p value))))
(ert-deftest ekp-gui-verify-g004-policy-context-is-json-primitive ()
"Policy context exposes stable JSON primitive values."
(let ((ekp-buffer--auto-width 280)
(ekp-inline-code-policy 'no-hyphen)
(ekp-hyphenation 'auto)
(ekp-kinsoku-profile 'liang)
(ekp-overlong-token-policy 'emergency))
(let ((policy (ekp-gui-verify--current-policy-context)))
(should (equal (cdr (assq 'inline_code policy)) "no-hyphen"))
(should (equal (cdr (assq 'hyphenation policy)) "auto"))
(should (equal (cdr (assq 'kinsoku policy)) "liang"))
(should (equal (cdr (assq 'overlong policy)) "emergency"))
(should (ekp-gui-verify--json-primitive-tree-p policy)))))
(ert-deftest ekp-gui-verify-g004-inline-break-map-ignores-external-break ()
"Final external line breaks do not invalidate internal inline break proof."
(with-temp-buffer
(let* ((source (make-string 220 ?x))
(range (cons (+ (point-min) 108) (+ (point-min) 198)))
(lines
(vector
(ekp-layout-line--create
:source-start 108 :source-end 136
:break-kind 'space
:break-source-start 136 :break-source-end 137)
(ekp-layout-line--create
:source-start 137 :source-end 173
:break-kind 'space
:break-source-start 173 :break-source-end 174)
(ekp-layout-line--create
:source-start 174 :source-end 198
:break-kind 'space
:break-source-start 198 :break-source-end 199)))
(plan (ekp-layout-plan--create
:string source
:lines lines))
ekp-buffer--spans)
(insert source)
(aset source 136 ?\s)
(aset source 173 ?\s)
(aset source 198 ?\s)
(setq ekp-buffer--spans
(list (ekp-buffer--span-create
:beg (copy-marker (point-min))
:end (copy-marker (point-max))
:width 280
:plan plan)))
(let ((evidence (ekp-gui-verify--inline-line-evidence range)))
(should (eq (cdr (assq 'automatic_inline_wrapped evidence)) t))
(should (eq (cdr (assq 'automatic_inline_breaks_source_spaces
evidence))
t))
(should (ekp-gui-verify--json-primitive-tree-p
(cdr (assq 'automatic_inline_break_map evidence))))
(should (equal
(cdr (assq 'kind
(car (cdr (assq 'automatic_inline_break_map
evidence)))))
"space"))))))
(ert-deftest ekp-gui-verify-g004-temporal-live-nil-plan-is-current ()
"Settled temporal assertions allow current live nil-plan spans."
(let* ((state '((stage . "policy-settled")
(width . 280)
(selected_target_window . t)
(source_unchanged . t)
(projection_published . t)
(generation_delta . 1)
(resize_timer . :false)
(policy_timer . :false)
(unplanned_spans . 1)
(live_unplanned_spans . 1)
(stale_unplanned_spans . 0)
(current_projection . t)
(timers_settled . t)
(pending_chunks . :false)
(overlays . 0)
(engine_c . t)
(automatic_inline_plan_complete . t)
(automatic_inline_line_count . 2)
(automatic_inline_wrapped . t)
(automatic_inline_breaks_source_spaces . t)
(isolated_cjk_cascades)
(isolated_cjk_lines)
(policy . ((inline_code . "normal")))
(completed . t)))
(assertions (ekp-gui-verify-showcase-temporal-assertions state)))
(dolist (name '("settled-zero-stale-unplanned-spans"
"settled-current-projection"))
(let ((assertion
(seq-find
(lambda (candidate)
(equal (cdr (assq 'name candidate)) name))
assertions)))
(should assertion)
(should (eq (cdr (assq 'passed assertion)) t))))))
(ert-deftest ekp-gui-verify-g004-temporal-unplanned-span-policy ()
"Allow unplanned spans only during the immediate pending policy stage."
(let* ((base '((width . 280)
(selected_target_window . t)
(source_unchanged . t)
(projection_published . t)
(generation_delta . 1)
(resize_timer . :false)
(pending_chunks . :false)
(overlays . 0)
(engine_c . t)
(automatic_inline_plan_complete . t)
(live_unplanned_spans . 0)
(stale_unplanned_spans . 0)
(live_state_current . t)
(automatic_inline_line_count . 2)
(automatic_inline_wrapped . t)
(automatic_inline_breaks_source_spaces . t)
(automatic_inline_break_map)
(isolated_cjk_cascades)
(isolated_cjk_lines)
(policy . ((inline_code . "normal")))))
(assertion-passed
(lambda (state name)
(let ((assertion
(seq-find
(lambda (candidate)
(equal (cdr (assq 'name candidate)) name))
(ekp-gui-verify-showcase-temporal-assertions state))))
(cdr (assq 'passed assertion))))))
(should
(eq (funcall assertion-passed
(append '((stage . "policy-immediate")
(policy_timer . t)
(unplanned_spans . 2)
(current_projection . :false)
(timers_settled . :false)
(completed . :false))
base)
"immediate-unplanned-spans-only-while-policy-pending")
t))
(should
(eq (funcall assertion-passed
(append '((stage . "policy-immediate")
(policy_timer . :false)
(unplanned_spans . 2)
(current_projection . :false)
(timers_settled . t)
(completed . :false))
base)
"immediate-unplanned-spans-only-while-policy-pending")
t))
(should
(eq (funcall assertion-passed
(append '((stage . "policy-settled")
(policy_timer . :false)
(unplanned_spans . 0)
(live_unplanned_spans . 0)
(stale_unplanned_spans . 0)
(current_projection . t)
(timers_settled . t)
(completed . t))
base)
"settled-zero-stale-unplanned-spans")
t))
(should
(eq (funcall assertion-passed
(append '((stage . "policy-settled")
(policy_timer . :false)
(unplanned_spans . 1)
(live_unplanned_spans . 0)
(stale_unplanned_spans . 1)
(current_projection . :false)
(timers_settled . t)
(completed . t))
base)
"settled-zero-stale-unplanned-spans")
:false))
(should
(eq (funcall assertion-passed
(append '((stage . "policy-settled")
(policy_timer . :false)
(unplanned_spans . 1)
(live_unplanned_spans . 0)
(stale_unplanned_spans . 1)
(current_projection . :false)
(timers_settled . t)
(completed . t))
base)
"settled-current-projection")
:false))))
(ert-deftest ekp-gui-verify-g004-temporal-width-requires-inline-wrap ()
"Manual narrow checkpoints fail when automatic inline evidence is unwrapped."
(let* ((state '((stage . "width-280")
(width . 280)
(selected_target_window . t)
(source_unchanged . t)
(projection_published . t)
(generation_delta . 0)
(resize_timer . :false)
(policy_timer . :false)
(unplanned_spans . 0)
(live_unplanned_spans . 0)
(stale_unplanned_spans . 0)
(live_state_current . t)
(current_projection . t)
(timers_settled . t)
(pending_chunks . :false)
(overlays . 0)
(engine_c . t)
(automatic_inline_plan_complete . t)
(automatic_inline_line_count . 1)
(automatic_inline_wrapped . :false)
(automatic_inline_breaks_source_spaces . :false)
(automatic_inline_break_map)
(isolated_cjk_cascades)
(isolated_cjk_lines)
(policy . ((inline_code . "no-hyphen")))
(completed . :false)))
(assertions (ekp-gui-verify-showcase-temporal-assertions state)))
(dolist (name '("manual-width-automatic-inline-wrapped"
"manual-width-inline-breaks-use-source-spaces"))
(let ((assertion
(seq-find
(lambda (candidate)
(equal (cdr (assq 'name candidate)) name))
assertions)))
(should assertion)
(should (eq (cdr (assq 'passed assertion)) :false))))))
(defun ekp-gui-verify--near-overflow-text (target)
"Return prose whose natural final line is nearly full at TARGET."
(let ((words ["alpha" "中文" "beta" "排版" "gamma" "编辑"])
(text "")
(index 0)
found)
(while (and (< index 300) (not found))
(setq text
(concat text
(if (string-empty-p text) "" " ")
(aref words (% index (length words)))))
(let* ((plan (ekp-layout-plan text target))
(lines (ekp-layout-plan-lines plan)))
(when (> (length lines) 2)
(let* ((line (aref lines (1- (length lines))))
(tail (substring
text
(ekp-layout-line-source-start line)
(ekp-layout-line-source-end line)))
(remaining (- target (ekp--measured-width tail))))
(setq found
(and (> remaining 0)
(< remaining
(ekp--measured-width
ekp-gui-verify--live-suffix)))))))
(setq index (1+ index)))
text))
(defun ekp-gui-verify--range-property-hash (beg end)
"Return an exact text-property hash for BEG through END."
(secure-hash
'sha256
(prin1-to-string (buffer-substring beg end))))
(defun ekp-gui-verify--configure-middle-edit ()
"Record one projected space and a later stable anchor range."
(let* ((spans (ekp-buffer--live-state-spans ekp-buffer--live-state))
(dirty (car spans))
(later (cadr spans))
(dirty-beg (marker-position (ekp-buffer--span-beg dirty)))
(dirty-end (marker-position (ekp-buffer--span-end dirty)))
(space
(save-excursion
(goto-char dirty-beg)
(search-forward " " dirty-end t))))
(unless (and space later)
(error "Live GUI setup needs one dirty row and one later anchor"))
(setq ekp-gui-verify--live-middle-position (copy-marker (1- space))
ekp-gui-verify--live-middle-later-beg
(copy-marker (marker-position (ekp-buffer--span-beg later)))
ekp-gui-verify--live-middle-later-end
(copy-marker
(marker-position (ekp-buffer--span-end (car (last spans)))) t)
ekp-gui-verify--live-middle-later-hash
(ekp-gui-verify--range-property-hash
ekp-gui-verify--live-middle-later-beg
ekp-gui-verify--live-middle-later-end)
ekp-gui-verify--live-middle-plan
(ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans spans)))
;;;###autoload
(defun ekp-gui-verify-live-setup ()
"Create a deterministic GUI buffer for live typing verification."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(set-frame-parameter (selected-frame) 'fullscreen 'fullboth)
(delete-other-windows)
(let ((buffer (get-buffer-create "*EKP Live Layout*")))
(switch-to-buffer buffer)
(when ekp-auto-justify-mode
(ekp-auto-justify-mode -1))
(let ((inhibit-read-only t))
(erase-buffer)
(text-mode)
(setq-local truncate-lines nil)
(redisplay t)
(insert (ekp-gui-verify--near-overflow-text
(ekp-buffer--effective-width)))
(setq ekp-gui-verify--live-original
(substring-no-properties (buffer-string))
ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-post-crossing-state nil
ekp-gui-verify--live-motion-generation nil
ekp-gui-verify--live-motion-cache-size nil
ekp-gui-verify--live-motion-state nil
ekp-gui-verify--live-stage "setup")
(ekp-auto-justify-mode 1)
(goto-char (point-max))
(run-hooks 'post-command-hook)
(redisplay t)
(setq ekp-gui-verify--live-original-signatures
(copy-tree
(ekp-buffer--live-state-signatures ekp-buffer--live-state))
ekp-gui-verify--live-original-projection-hash
(ekp-gui-verify--projection-hash)
ekp-gui-verify--live-original-active-index
(ekp-buffer--live-state-active-index ekp-buffer--live-state)
ekp-gui-verify--live-original-prefix-end
(marker-position
(ekp-buffer--live-state-prefix-end ekp-buffer--live-state))
ekp-gui-verify--live-original-width
ekp-buffer--auto-width
ekp-gui-verify--live-original-screen-lines
(count-screen-lines (point-min) (point-max)))
(ekp-gui-verify--configure-middle-edit))
(ekp-gui-verify-live-adapter)))
(defun ekp-gui-verify--owned-break-positions (&optional beg end)
"Return EKP-owned replacing-display newline positions from BEG to END."
(let ((position (or beg (point-min)))
(limit (or end (point-max)))
positions)
(while (< position limit)
(let* ((display (get-text-property
position 'ekp-buffer--display))
(next (or (next-single-property-change
position 'ekp-buffer--display nil limit)
limit)))
(when (and (stringp display)
(string-match-p
"\n" (substring-no-properties display)))
(push position positions))
(setq position next)))
(nreverse positions)))
(defun ekp-gui-verify--owned-property-runs ()
"Return stable descriptions of all EKP-owned projection runs."
(let ((position (point-min))
runs)
(while (< position (point-max))
(let* ((next (or (next-property-change
position nil (point-max))
(point-max)))
(display (get-text-property
position 'ekp-buffer--display))
(prefix (get-text-property
position 'ekp-buffer--line-prefix))
(justified (get-text-property position 'ekp-justified)))
(when (or display prefix justified)
(push (list position next display prefix justified) runs))
(setq position next)))
(nreverse runs)))
(defun ekp-gui-verify--projection-hash ()
"Return a stable hash of the current EKP projection."
(secure-hash
'sha256 (prin1-to-string (ekp-gui-verify--owned-property-runs))))
(defun ekp-gui-verify--stable-live-state ()
"Return the committed live projection state relevant to stability."
(let ((state ekp-buffer--live-state))
(list
:active (ekp-buffer--live-state-active-index state)
:prefix-end
(marker-position (ekp-buffer--live-state-prefix-end state))
:signatures (ekp-buffer--live-state-signatures state)
:projection (ekp-gui-verify--projection-hash)
:plan (ekp-buffer--live-state-plan state)
:spans (ekp-buffer--live-state-spans state))))
(defun ekp-gui-verify--stable-live-state-matches-p (snapshot)
"Return non-nil when the committed projection matches SNAPSHOT exactly."
(let ((current (ekp-gui-verify--stable-live-state)))
(and (eql (plist-get current :active)
(plist-get snapshot :active))
(eql (plist-get current :prefix-end)
(plist-get snapshot :prefix-end))
(eq (plist-get current :signatures)
(plist-get snapshot :signatures))
(equal (plist-get current :projection)
(plist-get snapshot :projection))
(eq (plist-get current :plan)
(plist-get snapshot :plan))
(eq (plist-get current :spans)
(plist-get snapshot :spans)))))
(defun ekp-gui-verify--dirty-live-span-p (span)
"Return non-nil when SPAN belongs to the current natural dirty island."
(when-let* ((edit ekp-buffer--live-edit)
(beg (marker-position
(ekp-buffer--live-edit-dirty-beg edit)))
(end (marker-position
(ekp-buffer--live-edit-dirty-end edit))))
(ekp-buffer--span-overlaps-p span beg end)))
(defun ekp-gui-verify--live-active-bounds ()
"Return the active semantic source range, or nil."
(if ekp-buffer--live-edit
(let ((beg (marker-position
(ekp-buffer--live-edit-dirty-beg ekp-buffer--live-edit)))
(end (marker-position
(ekp-buffer--live-edit-dirty-end ekp-buffer--live-edit))))
(and beg end (<= beg end) (cons beg end)))
(when-let* ((state ekp-buffer--live-state)
(plan (ekp-buffer--live-state-plan state))
(active (ekp-buffer--live-state-active-index state))
(beg (marker-position (ekp-buffer--live-state-beg state)))
(end (marker-position (ekp-buffer--live-state-end state))))
(let ((lines (ekp-layout-plan-lines plan)))
(when (< active (length lines))
(let ((line (aref lines active)))
(cons (+ beg (ekp-layout-line-source-start line)) end)))))))
(defun ekp-gui-verify--owned-layout-p (beg end)
"Return non-nil when BEG through END carries EKP layout."
(or (text-property-not-all beg end 'ekp-buffer--display nil)
(text-property-not-all beg end 'ekp-buffer--line-prefix nil)
(text-property-not-all beg end 'ekp-justified nil)))
(defun ekp-gui-verify--active-line-natural-p ()
"Return non-nil when the active semantic line has no EKP layout."
(when-let* ((bounds (ekp-gui-verify--live-active-bounds)))
(not (ekp-gui-verify--owned-layout-p
(car bounds) (cdr bounds)))))
(defun ekp-gui-verify--current-live-signatures ()
"Return the current semantic prefix signatures."
(and ekp-buffer--live-state
(ekp-buffer--live-state-signatures ekp-buffer--live-state)))
(defun ekp-gui-verify--stage-check (stage predicate)
"Return PREDICATE for STAGE, and true for every other stage."
(if (equal ekp-gui-verify--live-stage stage)
(if predicate t :false)
t))
(defun ekp-gui-verify-live-adapter ()
"Return JSON-compatible state for the live verification buffer."
(let* ((live-beg
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-beg ekp-buffer--live-state))))
(live-end
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-end ekp-buffer--live-state))))
(scan (ekp-gui-verify--scan
(current-buffer) #'ekp-gui-verify--dirty-live-span-p))
(logical (substring-no-properties (buffer-string)))
(breaks (ekp-gui-verify--owned-break-positions))
(live-breaks
(and live-beg live-end
(ekp-gui-verify--owned-break-positions live-beg live-end)))
(active (and ekp-buffer--live-state
(ekp-buffer--live-state-active-index
ekp-buffer--live-state)))
(prefix-end
(and ekp-buffer--live-state
(marker-position
(ekp-buffer--live-state-prefix-end
ekp-buffer--live-state))))
(signatures (ekp-gui-verify--current-live-signatures))
(projection-hash (ekp-gui-verify--projection-hash))
(later-hash
(and (markerp ekp-gui-verify--live-middle-later-beg)
(marker-position ekp-gui-verify--live-middle-later-beg)
(ekp-gui-verify--range-property-hash
ekp-gui-verify--live-middle-later-beg
ekp-gui-verify--live-middle-later-end)))
(original-p
(equal ekp-gui-verify--live-expected
ekp-gui-verify--live-original)))
`((logical_length . ,(length logical))
(logical_sha256 . ,(secure-hash 'sha256 logical))
(expected_source . ,(if (equal logical
ekp-gui-verify--live-expected)
t :false))
(source_newlines . ,(cl-count ?\n logical))
(owned_breaks . ,(length breaks))
(live_owned_breaks . ,(length live-breaks))
(stage . ,ekp-gui-verify--live-stage)
(projection_sha256 . ,projection-hash)
(active_index . ,(or active -1))
(stable_boundary_position . ,(or prefix-end -1))
(prefix_lines . ,(length signatures))
(plan_lines
. ,(if-let* ((plan (and ekp-buffer--live-state
(ekp-buffer--live-state-plan
ekp-buffer--live-state))))
(length (ekp-layout-plan-lines plan))
0))
(middle_anchor_preserved
. ,(ekp-gui-verify--stage-check
"middle-dirty"
(and ekp-buffer--live-edit
(equal later-hash
ekp-gui-verify--live-middle-later-hash)
(eq (ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-plan)
(eq (ekp-buffer--live-state-spans ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans))))
(middle_exact_restored
. ,(ekp-gui-verify--stage-check
"middle-restored"
(and original-p
(not ekp-buffer--live-edit)
(equal projection-hash
ekp-gui-verify--live-original-projection-hash)
(equal later-hash
ekp-gui-verify--live-middle-later-hash)
(eq (ekp-buffer--live-state-plan ekp-buffer--live-state)
ekp-gui-verify--live-middle-plan)
(eq (ekp-buffer--live-state-spans ekp-buffer--live-state)
ekp-gui-verify--live-middle-spans))))
(prefix_replanned
. ,(ekp-gui-verify--stage-check
"typed"
(and ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-post-crossing-state)
(not (equal signatures
ekp-gui-verify--live-original-signatures)))))
(delete_preserved_committed_projection
. ,(ekp-gui-verify--stage-check
"suffix-deleted"
(and original-p
ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-post-crossing-state))))
(point_move_back_preserved_projection
. ,(ekp-gui-verify--stage-check
"moved-back"
(and ekp-gui-verify--live-motion-generation
(= ekp-buffer--generation
ekp-gui-verify--live-motion-generation)
(= (length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-cache-size)
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(point_move_forward_preserved_projection
. ,(ekp-gui-verify--stage-check
"moved-forward"
(and ekp-gui-verify--live-motion-generation
(= ekp-buffer--generation
ekp-gui-verify--live-motion-generation)
(= (length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-cache-size)
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(yank_kept_current_row_natural
. ,(ekp-gui-verify--stage-check
"yanked"
(and ekp-buffer--live-edit
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(undo_preserved_committed_projection
. ,(ekp-gui-verify--stage-check
"undone"
(and original-p
ekp-buffer--live-edit
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state-matches-p
ekp-gui-verify--live-motion-state))))
(resize_reflowed
. ,(ekp-gui-verify--stage-check
"resized"
(< ekp-buffer--auto-width
ekp-gui-verify--live-original-width)))
(resize_restored
. ,(ekp-gui-verify--stage-check
"resize-restored"
(= ekp-buffer--auto-width
ekp-gui-verify--live-original-width)))
(completed_paragraph
. ,(if (or (not (equal ekp-gui-verify--live-stage "completed"))
(and (= (cl-count ?\n logical) 1)
(get-text-property (point-min) 'ekp-justified)
live-beg live-end
(= live-beg live-end (point-max))))
t :false))
(screen_lines . ,(count-screen-lines (point-min) (point-max)))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(hscroll . ,(window-hscroll))
(active_paragraph . ,(if ekp-buffer--live-state t :false))
(authoritative_width . ,(or ekp-buffer--auto-width 0))
(active_line_natural
. ,(if (or (member ekp-gui-verify--live-stage
'("completed" "resized" "resize-restored"))
(ekp-gui-verify--active-line-natural-p))
t :false))
(generation . ,ekp-buffer--generation)
(pending_edit . ,(if ekp-buffer--live-edit t :false))
(live_cache_size . ,(length ekp-buffer--live-plan-cache))
(pixel_scan_pass
. ,(if (or (= (or active 0) 0)
(plist-get scan :pass))
t :false))
(pixel_overflow_lines . ,(plist-get scan :over)))))
(defun ekp-gui-verify-live-assertions ()
"Return adapter assertions for the live verification buffer."
(let* ((adapter (ekp-gui-verify-live-adapter))
(value (lambda (key) (cdr (assq key adapter)))))
`(((name . "logical-source-exact")
(passed . ,(funcall value 'expected_source)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "zero-horizontal-scroll")
(passed . ,(if (= (funcall value 'hscroll) 0) t :false)))
((name . "middle-edit-preserves-later-anchor")
(passed . ,(funcall value 'middle_anchor_preserved)))
((name . "middle-reversal-restores-exact-projection")
(passed . ,(funcall value 'middle_exact_restored)))
((name . "visual-row-crossing-commits-prefix")
(passed . ,(funcall value 'prefix_replanned)))
((name . "delete-keeps-last-committed-projection")
(passed . ,(funcall value
'delete_preserved_committed_projection)))
((name . "point-back-preserves-projection")
(passed . ,(funcall value
'point_move_back_preserved_projection)))
((name . "point-forward-preserves-projection")
(passed . ,(funcall value
'point_move_forward_preserved_projection)))
((name . "same-row-yank-keeps-projection")
(passed . ,(funcall value 'yank_kept_current_row_natural)))
((name . "real-undo-keeps-committed-projection")
(passed . ,(funcall value
'undo_preserved_committed_projection)))
((name . "window-resize-reflows-prefix")
(passed . ,(funcall value 'resize_reflowed)))
((name . "window-width-restore-reflows-prefix")
(passed . ,(funcall value 'resize_restored)))
((name . "hard-newline-completes-previous-paragraph")
(passed . ,(funcall value 'completed_paragraph)))
((name . "active-semantic-line-natural")
(passed . ,(funcall value 'active_line_natural)))
((name . "live-cache-bounded")
(passed . ,(if (<= (funcall value 'live_cache_size) 16) t :false)))
((name . "no-pixel-overflow")
(passed . ,(if (= (funcall value 'pixel_overflow_lines) 0)
t :false))))))
;;;###autoload
(defun ekp-gui-verify-live-delete-middle-space ()
"Delete one projected middle-row space through the public command path."
(interactive)
(goto-char (1+ (marker-position
ekp-gui-verify--live-middle-position)))
(call-interactively #'delete-backward-char)
(let ((offset (- (marker-position
ekp-gui-verify--live-middle-position)
(point-min))))
(setq ekp-gui-verify--live-expected
(concat (substring ekp-gui-verify--live-original 0 offset)
(substring ekp-gui-verify--live-original (1+ offset)))
ekp-gui-verify--live-stage "middle-dirty"))
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-restore-middle-space ()
"Reinsert the deleted middle-row space and restore the exact baseline."
(interactive)
(goto-char ekp-gui-verify--live-middle-position)
(let ((last-command-event ?\s))
(call-interactively #'self-insert-command))
(setq ekp-gui-verify--live-expected ekp-gui-verify--live-original
ekp-gui-verify--live-stage "middle-restored")
(goto-char (point-max))
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-type-suffix ()
"Type the deterministic suffix through the command loop."
(interactive)
(setq ekp-gui-verify--live-stage "typed")
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original
ekp-gui-verify--live-suffix))
(execute-kbd-macro ekp-gui-verify--live-suffix)
(redisplay t)
(setq ekp-gui-verify--live-post-crossing-state
(ekp-gui-verify--stable-live-state))
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-delete-suffix ()
"Delete the suffix while retaining the last committed projection."
(interactive)
(execute-kbd-macro
(vconcat
(make-list (length ekp-gui-verify--live-suffix) 'backspace)))
(setq ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-stage "suffix-deleted")
(redisplay t)
(setq ekp-gui-verify--live-motion-generation ekp-buffer--generation
ekp-gui-verify--live-motion-cache-size
(length ekp-buffer--live-plan-cache)
ekp-gui-verify--live-motion-state
(ekp-gui-verify--stable-live-state))
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-move-back ()
"Move point into the first semantic line through the command loop."
(interactive)
(execute-kbd-macro (kbd "M-<"))
(setq ekp-gui-verify--live-stage "moved-back")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-move-forward ()
"Move point back to the final semantic line through the command loop."
(interactive)
(execute-kbd-macro (kbd "M->"))
(setq ekp-gui-verify--live-stage "moved-forward")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-yank ()
"Yank a multi-script suffix through the public command path."
(interactive)
(undo-boundary)
(kill-new ekp-gui-verify--live-yank)
(execute-kbd-macro (kbd "C-y"))
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original
ekp-gui-verify--live-yank)
ekp-gui-verify--live-stage "yanked")
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-undo-yank ()
"Undo the verification yank through the public command path."
(interactive)
(execute-kbd-macro (kbd "C-/"))
(setq ekp-gui-verify--live-expected
ekp-gui-verify--live-original
ekp-gui-verify--live-stage "undone")
(redisplay t)
(ekp-gui-verify-live-adapter))
(defun ekp-gui-verify--live-change-margin (columns stage)
"Set the right margin to COLUMNS and record verification STAGE."
(set-window-margins (selected-window) 0 columns)
(run-hooks 'window-configuration-change-hook)
(sit-for (+ ekp-auto-justify-resize-delay 0.2))
(setq ekp-gui-verify--live-stage stage)
(redisplay t)
(ekp-gui-verify-live-adapter))
;;;###autoload
(defun ekp-gui-verify-live-resize-narrower ()
"Narrow the live text area and wait for the real resize debounce."
(interactive)
(ekp-gui-verify--live-change-margin 24 "resized"))
;;;###autoload
(defun ekp-gui-verify-live-resize-restore ()
"Restore the live text area and wait for the real resize debounce."
(interactive)
(ekp-gui-verify--live-change-margin nil "resize-restored"))
;;;###autoload
(defun ekp-gui-verify-live-complete-paragraph ()
"Insert a hard newline and complete the previous paragraph."
(interactive)
(execute-kbd-macro "\n")
(setq ekp-gui-verify--live-expected
(concat ekp-gui-verify--live-original "\n")
ekp-gui-verify--live-stage "completed")
(redisplay t)
(ekp-gui-verify-live-adapter))
(defconst ekp-gui-verify--split-text
"soft wrap 中文 mixed editing stays natural across narrow side by side windows"
"Text typed by the split-window soft-wrap verification.")
(defvar-local ekp-gui-verify--split-expected "")
(defvar-local ekp-gui-verify--split-stage "setup")
(defvar-local ekp-gui-verify--split-original-wrap nil)
;;;###autoload
(defun ekp-gui-verify-split-wrap-setup ()
"Create an intentional narrow split for native soft-wrap verification."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(set-frame-parameter (selected-frame) 'fullscreen 'fullboth)
(delete-other-windows)
(redisplay t)
(sit-for 0.2)
(let* ((target (get-buffer-create "*EKP Split Soft Wrap*"))
(control (get-buffer-create "*EKP Split Control*"))
(left (selected-window))
(right (split-window-right)))
(set-window-buffer left target)
(set-window-buffer right control)
(window-resize left (- 44 (window-total-width left)) t)
(select-window left)
(with-current-buffer control
(erase-buffer)
(insert "Intentional control window for partial-width verification."))
(with-current-buffer target
(when ekp-auto-justify-mode
(ekp-auto-justify-mode -1))
(erase-buffer)
(text-mode)
(kill-local-variable 'truncate-lines)
(kill-local-variable 'truncate-partial-width-windows)
(setq ekp-gui-verify--split-expected ""
ekp-gui-verify--split-stage "setup"
ekp-gui-verify--split-original-wrap
(list (local-variable-p 'truncate-lines)
truncate-lines
(local-variable-p 'truncate-partial-width-windows)
truncate-partial-width-windows))
(ekp-auto-justify-mode 1)
(goto-char (point-max)))
(redisplay t)
(ekp-gui-verify-split-wrap-adapter)))
(defun ekp-gui-verify--split-row-start ()
"Return point's native visual-row start in the selected window."
(save-excursion
(vertical-motion 0 (selected-window))
(point)))
(defun ekp-gui-verify--split-display-state-restored-p ()
"Return non-nil when split verification restored its display state."
(pcase-let ((`(,lines-local ,lines ,partial-local ,partial)
ekp-gui-verify--split-original-wrap))
(and (eq (local-variable-p 'truncate-lines) lines-local)
(equal truncate-lines lines)
(eq (local-variable-p 'truncate-partial-width-windows)
partial-local)
(equal truncate-partial-width-windows partial))))
(defun ekp-gui-verify-split-wrap-adapter ()
"Return JSON-compatible state for split-window soft-wrap verification."
(let* ((logical (substring-no-properties (buffer-string)))
(typed-p (equal ekp-gui-verify--split-stage "typed"))
(disabled-p (equal ekp-gui-verify--split-stage "disabled"))
(breaks (ekp-gui-verify--owned-break-positions)))
`((stage . ,ekp-gui-verify--split-stage)
(expected_source
. ,(if (equal logical ekp-gui-verify--split-expected) t :false))
(logical_sha256 . ,(secure-hash 'sha256 logical))
(window_count . ,(length (window-list nil 'no-minibuf)))
(target_columns . ,(window-total-width))
(auto_mode . ,(if ekp-auto-justify-mode t :false))
(truncate_lines . ,(if truncate-lines t :false))
(partial_truncation
. ,(if truncate-partial-width-windows t :false))
(wrap_bindings_local
. ,(if (and (local-variable-p 'truncate-lines)
(local-variable-p 'truncate-partial-width-windows))
t :false))
(screen_lines . ,(count-screen-lines (point-min) (point-max)))
(visual_row_advanced
. ,(if (or (not typed-p)
(> (ekp-gui-verify--split-row-start) (point-min)))
t :false))
(semantic_prefix_lines
. ,(if ekp-buffer--live-state
(length
(ekp-buffer--live-state-signatures
ekp-buffer--live-state))
0))
(active_line_natural
. ,(if (or (not typed-p)
disabled-p
(ekp-gui-verify--active-line-natural-p))
t :false))
(hscroll . ,(window-hscroll))
(live_owned_breaks . ,(length breaks))
(overlays . ,(length (overlays-in (point-min) (point-max))))
(pending_edit . ,(if ekp-buffer--live-edit t :false))
(display_state_restored
. ,(if (or (not disabled-p)
(ekp-gui-verify--split-display-state-restored-p))
t :false)))))
(defun ekp-gui-verify-split-wrap-assertions ()
"Return assertions for split-window soft-wrap verification."
(let* ((adapter (ekp-gui-verify-split-wrap-adapter))
(value (lambda (key) (cdr (assq key adapter))))
(disabled-p
(equal (funcall value 'stage) "disabled")))
`(((name . "logical-source-exact")
(passed . ,(funcall value 'expected_source)))
((name . "intentional-two-window-layout")
(passed . ,(if (= (funcall value 'window_count) 2) t :false)))
((name . "target-window-below-default-truncation-threshold")
(passed . ,(if (< (funcall value 'target_columns) 50) t :false)))
((name . "auto-mode-owns-soft-wrap")
(passed . ,(if (or disabled-p
(and (funcall value 'auto_mode)
(eq (funcall value 'truncate_lines) :false)
(eq (funcall value 'partial_truncation) :false)
(funcall value 'wrap_bindings_local)))
t :false)))
((name . "typing-crosses-visual-boundary")
(passed . ,(funcall value 'visual_row_advanced)))
((name . "no-horizontal-scroll")
(passed . ,(if (or disabled-p
(= (funcall value 'hscroll) 0))
t :false)))
((name . "semantic-prefix-publishes-complete-lines")
(passed . ,(if (or (not (equal (funcall value 'stage) "typed"))
(and (> (funcall value 'semantic_prefix_lines) 0)
(> (funcall value 'live_owned_breaks) 0)))
t :false)))
((name . "active-semantic-line-natural")
(passed . ,(funcall value 'active_line_natural)))
((name . "zero-overlays")
(passed . ,(if (= (funcall value 'overlays) 0) t :false)))
((name . "no-pending-live-transaction")
(passed . ,(if (eq (funcall value 'pending_edit) :false)
t :false)))
((name . "mode-disable-restores-display-state")
(passed . ,(funcall value 'display_state_restored))))))
(defun ekp-gui-verify-split-generic-assertions
(window buffer start end)
"Return generic capture assertions for an intentional split layout."
(list
`((name . "selected-target-window-live")
(passed . ,(if (and (window-live-p window)
(eq (window-buffer window) buffer))
t :false)))
`((name . "intentional-two-window-capture")
(passed . ,(if (= (length (window-list nil 'no-minibuf)) 2)
t :false)))
`((name . "visible-range-valid")
(passed . ,(if (and (integer-or-marker-p start)
(integer-or-marker-p end)
(<= start end)
(<= end (with-current-buffer buffer (point-max))))
t :false)))))
;;;###autoload
(defun ekp-gui-verify-split-wrap-type ()
"Type across the native wrap boundary in the narrow target window."
(interactive)
(mapc
(lambda (character)
(let ((last-command-event character))
(call-interactively #'self-insert-command))
(redisplay t)
(sit-for 0.02))
(string-to-list ekp-gui-verify--split-text))
(setq ekp-gui-verify--split-expected ekp-gui-verify--split-text
ekp-gui-verify--split-stage "typed")
(redisplay t)
(ekp-gui-verify-split-wrap-adapter))
;;;###autoload
(defun ekp-gui-verify-split-wrap-disable ()
"Disable auto mode and expose restored truncation ownership."
(interactive)
(ekp-auto-justify-mode -1)
(setq ekp-gui-verify--split-stage "disabled")
(redisplay t)
(ekp-gui-verify-split-wrap-adapter))
;;;###autoload
(defun ekp-gui-verify-matrix ()
"Run the display-context matrix and print a PASS/FAIL table.
Covers: plain, text-scale up/down, family+height face remap, and a
narrow frame with scaling. Intended for `emacs -Q'; in a customized
session prefer `ekp-gui-verify'."
(interactive)
(unless (display-graphic-p)
(user-error "GUI verification needs a graphical frame"))
(save-current-buffer
(ekp-gui-verify--matrix-1)))
(defun ekp-gui-verify--matrix-1 ()
"Run the matrix cases; caller guards the current buffer."
(let (results)
(set-frame-size (selected-frame) 190 40)
(push (ekp-gui-verify--case "base" #'ignore) results)
(push (ekp-gui-verify--case "text-scale +3"
(lambda () (text-scale-set 3)))
results)
(push (ekp-gui-verify--case "text-scale -2"
(lambda () (text-scale-set -2)))
results)
(push (ekp-gui-verify--case "remap family+height"
(lambda ()
(face-remap-add-relative
'default :height 1.15)))
results)
(push (ekp-gui-verify--case "no fringes"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)))
results)
(push (ekp-gui-verify--case "no fringes + scale +2"
(lambda ()
(set-window-fringes
(get-buffer-window (current-buffer))
0 0)
(text-scale-set 2)))
results)
(set-frame-size (selected-frame) 70 40)
(push (ekp-gui-verify--case "narrow + scale +2"
(lambda () (text-scale-set 2)))
results)
(ekp-gui-verify--report (nreverse results))))
(provide 'ekp-gui-verify)
;;; ekp-gui-verify.el ends here