Compare commits

..

No commits in common. "main" and "kp-overhaul" have entirely different histories.

159 changed files with 2510 additions and 28877 deletions

View File

@ -1,171 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
name: elisp-only (Emacs ${{ matrix.emacs-version }})
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.emacs-version == 'snapshot' }}
strategy:
fail-fast: false
matrix:
# 29.1 is the declared floor (Package-Requires); 30.1 is current
# stable; snapshot is advisory (allowed to fail)
emacs-version: ['29.1', '30.1', 'snapshot']
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: purcell/setup-emacs@bdc64dc730ae1fcba200bfd52cb1b4cf6159cbe5 # v8.0
with:
version: ${{ matrix.emacs-version }}
- name: Byte-compile (warnings are errors)
run: |
emacs -Q --batch -L . \
--eval '(setq byte-compile-error-on-warn t)' \
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
- name: Run ERT suite (C-module tests auto-skip)
run: tests/run-tests.sh emacs
- name: Run ERT suite in permuted order
if: matrix.emacs-version == '30.1'
env:
EKP_TEST_SEED: '20260728'
run: tests/run-tests.sh emacs --random-order
lint:
name: package-lint + checkdoc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: purcell/setup-emacs@bdc64dc730ae1fcba200bfd52cb1b4cf6159cbe5 # v8.0
with:
version: '30.1'
- name: Check out pinned package-lint
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: purcell/package-lint
ref: 35996f478d81e51dae4fa30d051f741895d07399
path: .ci/package-lint
persist-credentials: false
- name: package-lint
run: |
emacs -Q --batch -L .ci/package-lint \
--eval "(require 'package-lint)" \
--eval "(setq package-lint-main-file \"ekp.el\")" \
-f package-lint-batch-and-exit \
ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
- name: checkdoc
run: |
emacs -Q --batch \
--eval "(setq sentence-end-double-space t)" \
--eval "(dolist (f '(\"ekp.el\" \"ekp-utils.el\" \"ekp-hyphen.el\" \"ekp-buffer.el\"))
(checkdoc-file f))" \
2>&1 | tee checkdoc.log
test ! -s checkdoc.log
- name: Release invariants
run: tests/check-release.sh
- name: Dictionary manifest and pinned source
run: |
tests/check-dictionaries.sh
dictionaries/update.sh check
test-c-module:
name: full suite + C parity (apt Emacs)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Install Emacs
run: sudo apt-get update && sudo apt-get install -y emacs-nox
- name: Ensure emacs-module.h is findable
run: |
if [ ! -e /usr/include/emacs-module.h ]; then
HDR=$(dpkg -L emacs-common emacs-bin-common 2>/dev/null | grep -m1 '/emacs-module\.h$' || true)
[ -n "$HDR" ] || HDR=$(sudo find /usr -name emacs-module.h 2>/dev/null | head -1)
test -n "$HDR" || { echo 'emacs-module.h not found'; exit 1; }
sudo install -m 644 "$HDR" /usr/local/include/
fi
- name: Build C module
run: make -C ekp_c
- name: Run ERT suite (with C module)
run: tests/run-tests.sh emacs
- name: Property fuzz (300 cases, C vs elisp parity)
run: emacs -Q --batch -L . -l tests/ekp-fuzz.el
test-c-sanitizers:
name: C module under ASan/UBSan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Install Emacs
run: sudo apt-get update && sudo apt-get install -y emacs-nox gcc
- name: Ensure emacs-module.h is findable
run: |
if [ ! -e /usr/include/emacs-module.h ]; then
HDR=$(dpkg -L emacs-common emacs-bin-common 2>/dev/null | grep -m1 '/emacs-module\.h$' || true)
[ -n "$HDR" ] || HDR=$(sudo find /usr -name emacs-module.h 2>/dev/null | head -1)
test -n "$HDR" || { echo 'emacs-module.h not found'; exit 1; }
sudo install -m 644 "$HDR" /usr/local/include/
fi
- name: Build C module with sanitizers
run: make -C ekp_c PROFILE=sanitize
- name: Property fuzz under ASan/UBSan
# Emacs itself is not ASan-instrumented: preload the runtime and
# skip leak checking (the Emacs process "leaks" by design).
run: |
LIBASAN=$(gcc -print-file-name=libasan.so)
LD_PRELOAD="$LIBASAN" ASAN_OPTIONS=detect_leaks=0 \
emacs -Q --batch -L . -l tests/ekp-fuzz.el
test-macos:
name: macOS (dylib build + full suite)
runs-on: macos-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: purcell/setup-emacs@bdc64dc730ae1fcba200bfd52cb1b4cf6159cbe5 # v8.0
with:
version: '30.1'
- name: Build C module
run: make -C ekp_c
- name: Run ERT suite (with C module)
run: tests/run-tests.sh emacs
- name: Property fuzz (300 cases, C vs elisp parity)
run: emacs -Q --batch -L . -l tests/ekp-fuzz.el
test-windows:
name: Windows Elisp baseline (Emacs 30.1)
runs-on: windows-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Install Emacs 30.1
shell: pwsh
run: choco install emacs --version=30.1.0 --yes --no-progress
- name: Byte-compile (warnings are errors)
shell: pwsh
run: >
emacs -Q --batch -L .
--eval "(setq byte-compile-error-on-warn t)"
-f batch-byte-compile
ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
- name: Run ERT suite (C-module tests auto-skip)
shell: pwsh
run: >
emacs -Q --batch -L . -L tests
--eval "(load-file \"tests/load-project-source.el\")"
-f ert-run-tests-batch-and-exit

2
.gitignore vendored
View File

@ -4,5 +4,3 @@ archive
*.dll *.dll
*.o *.o
*.elc *.elc
AGENTS.md
!AGENTS.md

View File

@ -1,122 +1,4 @@
# Change Log Index # Change Log Index
## phase-kp-overhaul-20260726
- 2026-09-01 restore Emacs 31.1 strict byte compilation without behavior
changes (`task044`, `issue029`):
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
- 2026-08-31 restore the Emacs 31 fresh-source baseline while preserving
multibyte policy ownership and real-buffer marker noninheritance
(`task043`):
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
- 2026-08-20 plan and implement post-audit hardening (`task037``task042`,
`task030` source-fresh evaluator continuation; `issue022``issue028`):
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
- 2026-08-20 select and implement native automatic live-append backend
(`task030`; `ekp-auto-justify-native-append`):
`.phrase/phases/phase-kp-overhaul-20260726/adr_native_live_append_backend_20260820.md`
- 2026-08-20 close the complete source-fresh live-append performance gate
(`task030`, `issue018`):
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
- 2026-08-13 close issues awaiting user-visible confirmation (`issue011`
`issue017`, `issue019`, `issue020`; `issue018`/`task030` remain open):
`.phrase/phases/phase-kp-overhaul-20260726/change_close_user_confirmation_20260813.md`
- 2026-08-02 complete fixed final-pass emergency stretch and TeX-style
active-path preservation (`task036`; `issue021` closed after user visual
confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_final_pass_emergency_stretch_20260802.md`
- 2026-07-28 repository system audit:
`.phrase/phases/phase-kp-overhaul-20260726/change_repository_audit_20260728.md`
- 2026-07-28 complete DP cache signature (`task002`):
`.phrase/phases/phase-kp-overhaul-20260726/change_cache_signature_20260728.md`
- 2026-07-28 complete automatic spacing cache identity (`task003`):
`.phrase/phases/phase-kp-overhaul-20260726/change_auto_spacing_signature_20260728.md`
- 2026-07-28 make saving non-mutating and failure-safe (`task004`):
`.phrase/phases/phase-kp-overhaul-20260726/change_save_transaction_20260728.md`
- 2026-07-28 compose and lifecycle-own buffer integrations (`task005`):
`.phrase/phases/phase-kp-overhaul-20260726/change_integration_lifecycle_20260728.md`
- 2026-07-28 eliminate ERT order-dependent false greens (`task006`):
`.phrase/phases/phase-kp-overhaul-20260726/change_test_isolation_20260728.md`
- 2026-07-28 make GUI verification fail closed (`task007`):
`.phrase/phases/phase-kp-overhaul-20260726/change_gui_verification_20260728.md`
- 2026-07-28 close the C API and arithmetic contract (`task008`):
`.phrase/phases/phase-kp-overhaul-20260726/change_c_api_contract_20260728.md`
- 2026-07-28 make C builds shell-free and profiled (`task009`):
`.phrase/phases/phase-kp-overhaul-20260726/change_c_build_boundary_20260728.md`
- 2026-07-28 close release and CI governance (`task010`):
`.phrase/phases/phase-kp-overhaul-20260726/change_release_governance_20260728.md`
- 2026-07-28 pin dictionary bytes and fail closed on unsupported syntax
(`task011`):
`.phrase/phases/phase-kp-overhaul-20260726/change_dictionary_governance_20260728.md`
- 2026-07-28 make measured text builders linear and cache nil (`task012`):
`.phrase/phases/phase-kp-overhaul-20260726/change_hot_loop_performance_20260728.md`
- 2026-07-28 clarify interactive protection workflows (`task013`):
`.phrase/phases/phase-kp-overhaul-20260726/change_interactive_protection_20260728.md`
- 2026-07-28 centralize duplicated core layout rules (`task014`):
`.phrase/phases/phase-kp-overhaul-20260726/change_core_rule_ownership_20260728.md`
- 2026-07-28 remove residual slop and pass the independent final gate
(`task015`):
`.phrase/phases/phase-kp-overhaul-20260726/change_final_cleanup_review_20260728.md`
- 2026-07-28 make the same-string paragraph fast path property-sensitive
(`task016`):
`.phrase/phases/phase-kp-overhaul-20260726/change_property_sensitive_fast_path_20260728.md`
- 2026-07-28 rename editor integration around its buffer owner (`task017`):
`.phrase/phases/phase-kp-overhaul-20260726/change_buffer_module_naming_20260728.md`
- 2026-07-28 record the logical-text API boundary (`issue011`, no
implementation):
`.phrase/phases/phase-kp-overhaul-20260726/change_logical_text_api_issue_20260728.md`
- 2026-07-29 record the text-property-only layout and non-polluting hyphen
feasibility (`task018`, `issue011` remains open):
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_research_20260729.md`
- 2026-07-29 implement and close the shared KP layout plan, source-clean
static projection, seamless live flow, and repository quality gate
(`task019``task022`; `issue011` awaits user-visible acceptance):
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
- 2026-07-29 fix and verify inactive-mark selection and same-turn
edge-whitespace visibility (`task023``task024`; `issue012` and
`issue013` await user-visible acceptance):
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
- 2026-07-29 replace partial live KP with the approved native progressive
editing model (`task025``task026` complete; `issue014` awaits
user-visible confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
- 2026-07-29 implement and verify the native soft-wrap lifecycle for narrow
split windows (`task027` complete; `issue015` awaits user-visible
confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_text_property_layout_implementation_20260729.md`
- 2026-07-29 implement and developer-verify semantic hard-line prefix live
editing (`task028`; `issue016` awaits user-visible confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_semantic_live_prefix_20260729.md`
- 2026-07-29 make point-only motion projection-stable and move the live
boundary to a source-edit-owned frontier (`task029` complete;
`issue016` and `issue017` await user-visible confirmation; separate
narrow-width append debt is `issue018`/`task030`):
`.phrase/phases/phase-kp-overhaul-20260726/change_editing_frontier_stability_20260729.md`
- 2026-07-29 implement and developer-verify the stable live-layout
transaction that supersedes per-keystroke frontier replanning
(`task031` complete; `issue019` awaits user confirmation; `task030`
is unblocked for structural-commit profiling):
`.phrase/phases/phase-kp-overhaul-20260726/change_stable_live_transaction_20260729.md`
- 2026-07-30 reduce core and complete C-backed resize p95 to
27.687/27.487 ms with exact layout parity (`task032` complete;
`issue020` awaits user confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_c_resize_latency_20260729.md`
- 2026-07-30 implement exact incremental live-append preparation, DP, and
projection reuse (`task030` implementation verified; locked
source-instrumented 16 ms gate and `issue018` remain open):
`.phrase/phases/phase-kp-overhaul-20260726/change_narrow_live_append_latency_20260730.md`
- 2026-08-01 implement and developer-verify the rigid-inline-atom
emergency-break correction (`task033` complete; `issue021` awaits user
confirmation):
`.phrase/phases/phase-kp-overhaul-20260726/change_overlong_no_break_atom_20260801.md`
- 2026-08-01 implement configurable break policies, record G002-G008
implementation/GUI/performance/repository/cleanup/review-blocker evidence,
and close developer work for `task034` (`issue021` still awaits user visual
confirmation; final independent code review APPROVE and architecture CLEAR):
`.phrase/phases/phase-kp-overhaul-20260726/change_configurable_break_policies_20260801.md`
- 2026-08-02 record the failed line-width emergency-stretch attempt
(`task035` closed as falsified; superseded by completed `task036` above):
`.phrase/phases/phase-kp-overhaul-20260726/change_final_pass_emergency_stretch_20260802.md`
## phase-doc-improvement-20260125 ## phase-doc-improvement-20260125
- See `.phrase/phases/phase-doc-improvement-20260125/change_log.md` - See `.phrase/phases/phase-doc-improvement-20260125/change_log.md`

View File

@ -1,31 +1,3 @@
# Known Issues # Known Issues
- issue001 [x] [Incomplete cache signatures reuse stale layout results.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md) (No open issues)
- issue002 [x] [Failed saves leave a justified buffer unformatted.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue003 [x] [Buffer integration is not composable or lifecycle-owned.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue004 [x] [Test order dependency produces a false green suite.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue005 [x] [C API and build boundary need explicit validation.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue006 [x] [Dictionary claims exceed parser and provenance coverage.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue007 [x] [GUI verification reports failure without failing automation.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue008 [x] [Hot-loop allocation and nil caching need benchmark gates.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue009 [x] [Release and documentation governance are not closed.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue010 [x] [Same-string property mutation bypasses paragraph cache identity.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
- issue011 [x] [Direct Elisp buffer APIs observe physical layout text.](../phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md)
- issue012 [x] [Reprojection activates an inactive mark and selects text.](../phases/phase-kp-overhaul-20260726/issue_live_editing_interaction_20260729.md#issue012--reprojection-activates-an-inactive-mark)
- issue013 [x] [Active-line edge whitespace is hidden until another glyph arrives.](../phases/phase-kp-overhaul-20260726/issue_live_editing_interaction_20260729.md#issue013--active-line-edge-whitespace-is-hidden)
- issue014 [x] [Near-edge live editing publishes KP breaks before the paragraph is complete.](../phases/phase-kp-overhaul-20260726/issue_natural_live_editing_20260729.md)
- issue015 [x] [Narrow side-by-side windows truncate instead of soft-wrapping live text.](../phases/phase-kp-overhaul-20260726/issue_native_soft_wrap_20260729.md)
- issue016 [x] [Live editing freezes native rows instead of projecting a semantic hard-line prefix.](../phases/phase-kp-overhaul-20260726/issue_semantic_live_prefix_20260729.md)
- issue017 [x] [Point-only motion changes an already published live projection.](../phases/phase-kp-overhaul-20260726/issue_point_motion_projection_stability_20260729.md)
- issue018 [ ] [Unique live appends miss the frame budget at very narrow widths.](../phases/phase-kp-overhaul-20260726/issue_narrow_live_append_latency_20260729.md)
- issue019 [x] [Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly.](../phases/phase-kp-overhaul-20260726/issue_stable_live_edit_transaction_20260729.md)
- issue020 [x] [C-backed resize reflow still takes roughly 6070 ms.](../phases/phase-kp-overhaul-20260726/issue_c_resize_latency_20260729.md)
- issue021 [x] [Final-pass emergency layout can isolate CJK source lines at narrow widths.](../phases/phase-kp-overhaul-20260726/issue_overlong_no_break_atom_20260801.md)
- issue022 [ ] [Invalid buffer widths mutate or create invalid projection state.](../phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md)
- issue023 [ ] [Backward deletion can republish a stable live projection.](../phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md)
- issue024 [ ] [Hyphenation locale spelling can select the wrong dictionary.](../phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md)
- issue025 [x] [C direct API accepts invalid break positions.](../phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md)
- issue026 [ ] [Showcase loses automatic inline policy and test runners can false-green.](../phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md)
- issue027 [x] [Manual integrations remain installed with no projection owner.](../phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md)
- issue028 [x] [Locked live evaluator can measure stale candidate bytecode.](../phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md)
- issue029 [x] [Emacs 31.1 WERROR rejects obsolete single-binding let macros.](../phases/phase-kp-overhaul-20260726/issue_emacs31_strict_compile_20260901.md)

View File

@ -35,7 +35,7 @@ agent-browser close # Close browser
```bash ```bash
agent-browser open <url> # Navigate to URL agent-browser open <url> # Navigate to URL
agent-browser back # Go back agent-browser back # Go back
agent-browser forward # Go forward agent-browser forward # Go forward
agent-browser reload # Reload page agent-browser reload # Reload page
agent-browser close # Close browser agent-browser close # Close browser
``` ```
@ -147,4 +147,4 @@ agent-browser get text @e1 --json
agent-browser open example.com --headed # Show browser window agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages agent-browser console # View console messages
agent-browser errors # View page errors agent-browser errors # View page errors
``` ```

View File

@ -1,12 +1,5 @@
# Change Log: Phase Doc Improvement 20260125 # Change Log: Phase Doc Improvement 20260125
## 2026-07-28
- **Modify**: `task_doc_improvement.md`
- Backfilled task001-task006 completion markers from the phase's existing
change log and delivered files; no product or documentation behavior
changed.
## 2026-01-25 ## 2026-01-25
- **Add**: `DEVELOPER.md` and `DEVELOPER_ZH.md` - **Add**: `DEVELOPER.md` and `DEVELOPER_ZH.md`

View File

@ -1,8 +1,8 @@
# Tasks: Documentation Improvement # Tasks: Documentation Improvement
- task001 [x] Create `DEVELOPER.md` with extracted technical content from `readme.md` - task001 [ ] Create `DEVELOPER.md` with extracted technical content from `readme.md`
- task002 [x] Create `DEVELOPER_ZH.md` with extracted technical content from `readme_zh.md` - task002 [ ] Create `DEVELOPER_ZH.md` with extracted technical content from `readme_zh.md`
- task003 [x] Enhance `DEVELOPER.md` with detailed Elisp API and C Module internals - task003 [ ] Enhance `DEVELOPER.md` with detailed Elisp API and C Module internals
- task004 [x] Enhance `DEVELOPER_ZH.md` with detailed Elisp API and C Module internals - task004 [ ] Enhance `DEVELOPER_ZH.md` with detailed Elisp API and C Module internals
- task005 [x] Refine `readme.md` to be user-focused (remove internal details, add links to Dev docs) - task005 [ ] Refine `readme.md` to be user-focused (remove internal details, add links to Dev docs)
- task006 [x] Refine `readme_zh.md` to be user-focused (remove internal details, add links to Dev docs) - task006 [ ] Refine `readme_zh.md` to be user-focused (remove internal details, add links to Dev docs)

View File

@ -1,10 +1,5 @@
# KP 算法系统性优化 — 进度交接文档 # KP 算法系统性优化 — 进度交接文档
> **2026-07-28 superseded note:** 本文的“未完成事项”保留为 2026-07-26
> 历史快照;后续审计、整改和验证状态以
> `task_repository_audit_20260728.md``.phrase/docs/ISSUES.md`
> 为准。
>
> 阶段:phase-kp-overhaul-20260726 > 阶段:phase-kp-overhaul-20260726
> 状态:**已完成并提交**——分支 `kp-overhaul`(基于 main@11437cb), > 状态:**已完成并提交**——分支 `kp-overhaul`(基于 main@11437cb),
> 合并到 main:`git checkout main && git merge kp-overhaul` > 合并到 main:`git checkout main && git merge kp-overhaul`

View File

@ -1,52 +0,0 @@
# ADR: Native Backend for Automatic Live Append 2026-08-20
## Context
Source-loaded automatic live append exceeded the interaction budget because
the strict Elisp append DP interpreted every structural transition. The
existing C backend already accepts the prepared paragraph arrays and produces
exactly the same break result, but `ekp-use-c-module=nil` previously disabled
it even inside the live append path.
## Decision
Add `ekp-auto-justify-native-append`, defaulting to non-nil. When auto mode is
publishing an already prepared, context-safe 1D live append and the compatible
C module is loaded, `ekp--dp-cache-append` may use the native DP regardless of
the ordinary full-layout `ekp-use-c-module` setting. The string API and full
paragraph layout continue to obey `ekp-use-c-module` directly. Setting the new
option to nil restores pure Elisp live append; an unavailable module always
falls back to Elisp.
The native call receives the same prepared 15-field arrays and is validated by
the existing C/Elisp parity contract. No C ABI field or source projection
representation changes.
## Alternatives
- Duplicate the strict Elisp DP for append: rejected after the parity
experiment required a second 160-line transition kernel and returned nil.
- Reuse final-pass transient state: rejected because artificial candidates and
surviving-path arrays are not part of the persisted state.
- Keep the source stress debt open: insufficient after the user selected the
native live-append architecture.
## Consequences
- A loaded C module accelerates live append even when full layout is explicitly
configured for Elisp; this is documented and user-controllable.
- The remaining source latency belongs to Elisp-owned append preparation and
semantic plan assembly, which remain exact and testable.
- Native-unavailable environments retain the previous pure-Elisp behavior.
## Verification
- A public buffer regression proves native calls occur only when the option is
enabled and are absent when it is disabled.
- Existing append-chain, C/Elisp parity, fuzz, and source-clean tests remain
required; source-fresh evaluator reports native-live usage explicitly.
## Rollback
Set `ekp-auto-justify-native-append` to nil or revert the dispatch change;
the full layout API and valid C contract remain independently usable.

View File

@ -1,38 +0,0 @@
# Change Log: Automatic Spacing Cache Identity 2026-07-28
## task003
- **Modify**`ekp.el`
- Added one spacing signature used by the paragraph hash and
`ekp--last-para`.
- Automatic mode records `ekp-default-cws-stretch-pixel`; explicit mode
records all nine spacing values.
- Kept font-derived automatic inputs owned by the existing font and
display-context identity.
- **Modify**`tests/ekp-tests.el`
- Added separate regressions for paragraph-table reuse and the same-string
fast path.
- Added an identity/count control proving unchanged signatures still hit.
- **Modify** — public, developer, audit, and phase documentation
- Documented the complete spacing identity and closed `issue001`.
- Added the decision record
`postmortem/20260728-paragraph-spacing-signature.md`.
## Validation
- Focused red: 1/3 passed; both stale-value regressions returned 2 instead
of the new default 9.
- Focused green: 3/3.
- Full ERT: 99/99.
- C/Elisp fuzz: 300/300.
- Byte compilation with warnings as errors: clean.
- checkdoc: clean.
## Behavior and Risk
- Changing automatic CJK stretch takes effect on the next call without
clearing caches.
- Returning to an earlier signature may reuse its still-valid paragraph.
- No public API, C ABI, saved-file format, dependency, or watcher changed.

View File

@ -1,61 +0,0 @@
# Change Log: Buffer Module Naming 2026-07-28
## task017
- **Delete/Add**`ekp-region.el``ekp-buffer.el`
- Renamed the editor integration file and provided feature around the
buffer lifecycle owner.
- Renamed module configuration and private symbols from `ekp-region*` to
`ekp-buffer*`.
- Attached the buffer customization subgroup to the top-level `ekp`
group and made the protrusion-reserve helper explicitly private.
- Kept public commands whose names correctly describe region, buffer, or
mode behavior.
- **Delete/Add**`tests/ekp-region-tests.el`
`tests/ekp-buffer-tests.el`
- Renamed the test feature, fixtures, and ERT names.
- Updated default, random-order, isolated, GUI, and showcase loaders.
- **Modify** — CI, contribution, public/developer/audit documentation
- Updated compilation, lint, checkdoc, test, and Windows CI paths.
- Replaced the documented require/configuration surface and file maps.
- Added the breaking migration to `CHANGELOG.md`.
- Recorded the ownership decision in
`postmortem/20260728-buffer-module-naming.md`.
## Cleanup Review
- Naming mismatch: resolved at the owning module/feature/configuration/test
boundary.
- Dead code, duplication, error handling, dependency, and UI: unchanged and
outside this pass.
- Fallback findings: only the existing selected-window width fallback,
classified as a grounded undisplayed-buffer boundary.
- No masking fallback, swallowed error, compatibility shim, new dependency,
new abstraction, or unrelated refactor was introduced.
## Validation
- Pre-change behavior lock: ERT 130/130.
- Focused buffer ERT after rename: 44/44.
- Default and seeded-permuted ERT: 130/130 each.
- Fresh-process isolation: every one of 130 selected ERT tests passed.
- C/Elisp property fuzz: 300/300.
- Warning-as-error production byte compilation: pass.
- Checkdoc and pinned package-lint: pass.
- Release invariants, shell syntax, diff check, and active-surface stale-name
scan: pass.
- Mechanical runtime/test equivalence against the approved rename table:
pass.
## Behavior and Risk
- Layout, serialization, search, copy, undo, and automatic reflow behavior
are unchanged.
- This is an intentional source-level breaking rename: users must require
`ekp-buffer` and rename any `ekp-region-*` settings.
- No saved-file format, dependency, Emacs baseline, C ABI, or release
artifact changed.
- Generated `.elc` verification artifacts were removed after compilation;
they can be regenerated from the source.

View File

@ -1,36 +0,0 @@
# Change Log: C API and Arithmetic Contract 2026-07-28
## task008
- **Modify**`ekp_c/ekp.c`, `ekp_c/ekp_module.h`, `ekp_c/ekp_kp.c`
- Preflighted all single/batch schema fields before extraction.
- Added `ekp-c-invalid-input` and atomic penalty validation.
- Widened line arithmetic and stored rest values to 64-bit.
- Bumped the module to 1.6.
- **Modify**`ekp.el`, `ekp-utils.el`
- Required C 1.6.
- Preserved nil fallback but propagated enabled-backend signals.
- **Add/Modify** — C boundary tests, loaders, and documentation
- Added six direct module cases and a public dispatch error control.
- Updated public/developer/C API contracts and audit records.
## Validation
- Direct C boundary red/green: 0/6 → 6/6.
- Public dispatcher signal red/green: 0/1 → 1/1.
- Release C build: C11, `-Wall -Wextra -Wpedantic`, zero warnings.
- Sanitizer C build: ASan/UBSan, zero compile warnings. Loading the ASan
dylib into signed macOS Emacs was blocked by platform runtime policy, so
runtime sanitizer evidence remains delegated to Linux CI.
- Full ERT: 116/116.
- C/Elisp fuzz: 300/300.
## Behavior and Risk
- Valid C calls retain the same arguments and result shape.
- Invalid direct calls now use a stable, specific error condition.
- Allocation/no-result remains recoverable via Elisp; module signals expose
broken internal contracts instead of hiding them.
- Older 1.5 modules are rejected until rebuilt.

View File

@ -1,39 +0,0 @@
# Change Log: Shell-Free Profiled C Builds 2026-07-28
## Planning
- Added `task009` and locked its process, profile, whitespace-path, and
build/test acceptance criteria before implementation.
## task009
- **Modify**`ekp-utils.el`
- Deleted the one-use generic callback wrapper and shell command.
- Added direct argv `make-process`, explicit profile validation, correct
working directory, and success/failure output lifecycle.
- **Modify**`ekp_c/Makefile`, `.github/workflows/ci.yml`
- Made portable flags the default.
- Split native, debug, and sanitizer flags behind `PROFILE`.
- Updated sanitizer CI to the same vocabulary.
- **Modify** — tests and public/developer/C/audit documentation
- Added process-shape and unknown-profile controls.
- Documented profile ownership and closed `issue005`.
## Validation
- Focused process/profile red/green: 0/2 → 2/2.
- portable/native/debug/sanitize: all compiled with zero warnings.
- Real `ekp-c-module-build 'portable`: exit 0 and loaded C 1.6.
- Portable copy under `/tmp/ekp build.*`: built successfully.
- Full ERT and fuzz evidence from `task008` remains valid; final phase QA
reruns both after all slices.
## Behavior and Risk
- `portable` no longer emits host-specific instructions.
- `native` artifacts are intentionally machine-specific.
- Failed async builds keep and display their output buffer; successful
builds reload and remove it.
- The old `DEBUG=1` spelling is deleted rather than retained as a shim.

View File

@ -1,39 +0,0 @@
# Change: C Resize Latency 2026-07-29
## 2026-07-30 — Complete task032 core and resize optimization
- **Modify** `ekp.el`: resolve a paragraph once per layout plan, reuse its
prepared DP data, memoize width-independent natural gap geometry, and omit
true zero-source/zero-target projection gaps.
- **Modify** `ekp-buffer.el`: publish owned properties in one mutation,
consume prepared gap geometry without projection-time measurement, and
keep the active paragraph out of the static resize pass before installing
its live prefix.
- **Add** focused RED/GREEN regressions in `tests/ekp-tests.el` and
`tests/ekp-buffer-tests.el`, plus the frozen dual-path evaluator in
`tests/ekp-c-resize-evaluator.el` and
`tests/run-c-resize-evaluator.sh`.
- **Performance:** four interleaved rounds reduce core p95 from 42.006 ms to
27.687 ms (34.09%) and complete resize p95 from 46.611 ms to 27.487 ms
(41.03%); both p50 gains also exceed 20%.
- **Correctness:** frozen-C, candidate-C, and Elisp projection hashes match.
Normal, random-order, and isolated ERT gates pass; 300 fuzz cases, static
checks, release checks, and reviewed temporal GUI evidence pass.
- **Risk:** external window-system allocation can trigger an Emacs GC pause
at the resize callback boundary. Total, GC, and EKP mutator time remain
separately observable; no debounce or global GC behavior changed.
## 2026-07-29 — Plan issue020 and task032
- **Add** `issue020` and `task032` for the reported 6070 ms C-backed
resize/reflow latency.
- **Add** the `c-resize-latency` performance-goal contract before
optimization: paired p50/p95 improvement of at least 20%, candidate p95
at or below 50 ms, exact frozen-C and Elisp parity, and complete
regression gates.
- **Decision:** measure complete reflow, plan construction, Emacs/C
marshalling, C DP, and projection publication separately before choosing
the implementation layer.
- **Risk:** a whole-reflow stopwatch can misattribute Elisp measurement or
property publication to the C algorithm. No runtime change is included in
this planning entry.

View File

@ -1,48 +0,0 @@
# Change Log: Complete DP Cache Signature 2026-07-28
## task002
- **Modify**`ekp.el`
- Replaced the width/looseness special-case key with one flat signature
containing every runtime DP input not frozen into `ekp-para`.
- Changed each paragraph's DP cache from `eql` to structural `equal`
comparison.
- Result: parameter changes cannot alias stale results, and structurally
identical non-zero-looseness signatures now hit the cache.
- **Modify**`tests/ekp-tests.el`
- Added six parameter-by-parameter cached-vs-fresh regressions through
`ekp-dp-cache`.
- Added an identity/count assertion proving the fix preserves real cache
hits instead of merely forcing recomputation.
- **Modify** — public and developer documentation
- Removed the temporary manual-cache-clear workaround.
- Documented the complete signature and added an Unreleased changelog
entry.
- Preserved the dated audit evidence while recording the follow-up status.
- **Add**`postmortem/20260728-dp-cache-signature.md`
- Recorded why a complete structural key was chosen over watchers,
collision-prone hashes, or another mutable signature owner.
## Validation
- Focused red: 0/2 expected tests passed on the old key.
- Focused green: 2/2 passed on the complete signature.
- Full source ERT: 96/96.
- Full compiled ERT: 96/96.
- C/Elisp fuzz: 300/300.
- Byte compilation with warnings as errors: clean.
- checkdoc: clean.
- Public rendered-output probe: cached and fresh results match in both
Elisp and C modes after a penalty change.
- 100,000 cache-hit microbenchmark: approximately 0.37 seconds versus
0.17 seconds for the old scalar key (about 2 microseconds per lookup).
## Risk
- Cache keys are small structural lists instead of scalar/cons `eql` keys.
- Deliberately sweeping many parameter combinations retains one result per
signature until the paragraph cache is cleared.
- No public API, C ABI, paragraph format, or saved-file format changed.

View File

@ -1,22 +0,0 @@
# Change: Close User-Confirmation Issues 2026-08-13
## 2026-08-13 — Close issues awaiting user-visible confirmation
- **Modify** `.phrase/docs/ISSUES.md` to mark `issue011``issue017`,
`issue019`, and `issue020` as resolved.
- **Modify** the corresponding issue detail files to record the closure
date, responsible closure path, and that the commit is
documentation-only.
- **Leave open** `issue018`/`task030`: the locked source-instrumented
16 ms stress target is still unmet, so that item remains an active
engineering debt rather than a pending user-confirmation gate.
- **Behavior/Risk:** no runtime change. This pass closes the required
user-visible acceptance gate for work that had already completed
developer, repository, and dynamic GUI verification.
## Verification
- `issue011``issue017`, `issue019`, and `issue020` now use `[x]` in the
global issue index and their detail headers.
- `issue018` remains `[ ]`; `task030` remains `[ ]`.
- No production or test code was changed.

View File

@ -1,175 +0,0 @@
# Change: Configurable Break Policies 2026-08-01
Superseded note: the historical hard-atom adjacency rule recorded below was
removed by `task036`. Explicit atoms forbid only interior breaks; current
final-pass reachability semantics are recorded in
`change_final_pass_emergency_stretch_20260802.md`.
## 2026-08-01 — Plan task034 and record the red baseline
- **Add** `task034` for configurable break policies and the remaining
orphan-glyph quality failure in the 280px showcase paragraph.
- **Modify** the text-property layout spec to make inline code wrapping,
token policies, kinsoku profiles, overlong-token behavior, buffer measure,
and region break-policy precedence explicit.
- **Modify** the text-property layout plan with M13, preserving the approved
architecture: resolve policy intervals before tokenization, compile to the
existing hyphen/break vectors before DP, and keep the C boundary at 15
arguments and 15 batch fields.
- **Red baseline:** the latest screenshot shows the previous atom cascade is
gone, but the crafted inline-code paragraph still permits pathological
single-CJK source lines around the atom, including `行`, `内`, and `永`.
The implementation oracle must reject any such single-CJK source line when
a legal non-emergency alternative exists.
- **Verification contract:** focused RED/GREEN ERT for core, buffer, command,
cache, and diagnostics; C/Elisp parity; 300-case property fuzz;
warning-as-error Elisp compilation; C builds/tests; checkdoc/package/static
release gates; and reviewed fullscreen GUI evidence for inline wrapping,
no orphan glyph, explicit no-break, block verbatim, measure modes, and
overlong-token modes.
- **Behavior/Risk:** Planning records only. Runtime behavior is unchanged.
This entry opened `task034`; later entries record its implementation and
closure.
## 2026-08-01 — Record task034 implementation and G004 GUI evidence
- **Modify** `task034`, M13, and the text-property layout spec with verified
implementation facts from G002/G003/G004. At this point `task034` was still
open pending later gates; G006 closure is recorded below.
- **Implementation evidence recorded:** core policy compilation preserves the
existing C boundary; buffer policy ownership separates block skip faces from
inline faces; public local/profile/region controls and diagnostics exist;
showcase verification distinguishes automatic inline code from explicit
`ekp-no-break` and verbatim block code.
- **GUI evidence recorded:** G004 passed GUI verifier ERT 7/7 and the clean
single-window evidence run at `/tmp/ekp-g004-evidence.Tp77dW` reports 12/12
checkpoints with no failed assertions, 25.75s/206-frame recording, no black
segments, automatic inline wrapping 3→2→3 lines across 280→340→280, every
internal inline split as source whitespace, exact source, zero overlays, C
active, and a settled no-hyphen→normal policy transition with stale
nil-plan spans at zero.
- **Verification status:** G005 repository-wide gates and G006 cleanup are
recorded below.
- **Behavior/Risk:** Documentation synchronization only in this entry. Do not
close `task034` or any user-confirmation issue from this G004-only evidence
at this point in the sequence.
## 2026-08-01 — Record G005 final performance and repository gate evidence
- **Modify** `task034` and the policy-boundary postmortem with the confirmed
G005 root causes and final gate evidence. At this point `task034` was still
open pending G006 cleanup; G006 closure is recorded below.
- **Root causes recorded:** paragraph identity included unconditional policy
measure, causing paragraph rebuilds; volatile property clearing fragmented
equal property runs; policy analysis ran before paragraph-cache hits; and
repeated same-paragraph/same-width requests duplicated semantic plan
assembly.
- **Fix evidence recorded:** policy measure was removed from the semantic
signature; adjacent equal filtered intervals are canonicalized; policy full
analysis now uses a bounded two-tier cache keyed by clean source, canonical
layout and raw face policy intervals, policy signature, and width context,
with a measure tier only for actual width-sensitive no-break/overflow
candidates; and semantic plans use a bounded per-paragraph plan cache. G007
later strengthened the consumer boundary from shallow copies to
consumer-owned copies of plan-owned mutable payloads; see the G008 entry
below.
- **Performance evidence recorded:** formal four-interleaved evaluator reports
layout parity true. Core baseline p50/p95 was 38.6679/51.6782ms and
candidate p50/p95 was 21.7102/33.0040ms, for 43.8549%/36.1354% gains.
Resize baseline p50/p95 was 43.3831/55.6250ms and candidate p50/p95 was
22.0919/32.8202ms, for 49.0773%/40.9973% gains. Both candidate p95 values
are under 50ms and both gain sets exceed 20%.
- **Live evaluator recorded:** the source-instrumented locked goal remains the
known `validation_failed` debt; parity, zero-work, GC, conflict, and
all-width-nonregression checks are true. Current C p95/p99 is
26.449/26.740ms, a 76.65%/77.23% improvement. Current Elisp p95/p99 is
49.940/52.017ms, a 91.64%/91.35% improvement. This is consistent with
historical open `issue018` and is not a regression.
- **Production public-path evidence recorded:** three byte-compiled runs
passed with zero GC. C append p99 was 1.361-1.368ms and hard p99 was
1.876-1.891ms. Elisp append p99 was 1.692-1.775ms and hard p99 was
2.100-2.230ms.
- **Verification recorded:** default ERT passed 262/262; seeded permuted ERT
seed 20260728 passed 255/255; isolated per-test process suite exited 0;
the subsequently added alias guard passed targeted verification; property
fuzz passed 300/300; warning-as-error byte compilation passed; pinned
package-lint at `35996f478d81e51dae4fa30d051f741895d07399` exited 0 with
only an external obsolete warning from the local names dependency; checkdoc
was empty; release, 49-entry dictionary manifest, pinned dictionary update,
shell syntax, CI YAML, and diff-check passed; portable, native, debug, and
sanitize C builds were warning-clean; module 1.6/4-thread smoke passed; and
focused C verification passed 19/19 including the 15-argument/15-field
boundary.
- **Verification status:** G005 gates are complete. G006 cleanup is recorded
below.
## 2026-08-01 — Close task034 with G006 cleanup evidence
- **Modify** `task034`, M13, the global change index, and the policy-boundary
postmortem with G006 cleanup evidence.
- **Cleanup evidence recorded:** G006 edited only `ekp.el` and
`ekp-buffer.el`, removing redundant policy/cache code without changing the
locked behavior.
- **Verification recorded:** targeted cleanup suites passed 6/6 and 3/3; full
core ERT passed 125/125; full buffer ERT passed 120/120; warning-as-error
byte compilation passed; diff-check passed; and the C build gate passed.
- **Behavior/Risk:** `task034` is closed for developer implementation,
repository gates, performance gates, and cleanup gates. `issue021` remains
open pending user visual confirmation. Independent final code/architecture
review has not yet been claimed.
## 2026-08-01 — Resolve G006/G007 review blockers with G008 ownership fixes
- **Modify** `task034`, M13, developer documentation, changelog, and
postmortems with the G007/G008 ownership and backend-contract corrections.
- **Review blockers resolved:** G006 cleanup's independent review found that
shallow semantic-plan copies left nested line/gap/glue payloads mutable
through cache hits, and that malformed non-nil C results could still be
treated too softly. G007 changed the semantic-plan cache boundary to return
consumer-owned copies of every plan-owned mutable payload: source string,
context, boxes, offsets, lines, glues, gaps, and signatures. `para` remains
intentionally shared because paragraph-cache ownership and append identity
rely on that object. G007 final review then found one remaining alias:
`copy-tree` did not copy strings inside the returned context, so mutating a
dynamic policy suffix from the returned plan could poison the cached context.
G008 resolves that final blocker by using the recursive context copier for
cons/vector/string payloads both when the context snapshot/cache key is
created and when a plan is returned.
- **Backend contract recorded:** only a nil whole C result or nil per-item
breaks may fall back to Elisp. Any malformed non-nil single or batch output
signals `ekp-backend-contract-error`. The C entry remains 15 arguments and
the batch payload remains 15 fields.
- **Hard-atom boundary recorded:** the ordinary boundary immediately before a
following explicit `ekp-no-break` atom is forbidden before DP. This direct
pre-DP adjacency rule is separate from the final-pass emergency-stretch
model and is locked by a focused regression.
- **Verification recorded:** formal resize parity is true; core baseline
p50/p95 is 36.595/48.357ms and candidate p50/p95 is 23.266/36.363ms, for
36.42%/24.80% gains. Resize baseline p50/p95 is 41.443/53.541ms and
candidate p50/p95 is 23.761/36.889ms, for 42.67%/31.10% gains. ERT passes
268/268; property fuzz passes 300/300; release gates pass; byte-compiled
current public path records zero GC with C append/hard p99
1.440-1.464/2.022-2.050ms and Elisp append/hard p99
1.648-1.687/2.136-2.335ms, all below 16ms.
- **Behavior/Risk:** G007 resolved the first recorded G006 review blockers,
and G008 resolves the final string-leaf context blocker, but this change
record does not claim final independent APPROVE/CLEAR. `issue021` remains
open pending user visual confirmation, and historical `issue018` remains
open.
## 2026-08-02 — Record final independent review clearance
- **Review remediation:** the first final code-review pass found warning-only
defects in changed test fixtures and a stale C README setter example. The
fixtures now byte-compile with warnings as errors, and the README documents
the optional eighth `EMERGENCY-STRETCH` argument.
- **Verification:** the remediation-focused ERT passes 5/5, full ERT passes
288/288, every changed test file compiles warning-clean, and diff-check
passes.
- **Independent verdicts:** the code-reviewer re-review returns `APPROVE` with
zero findings; the subsequent architect review returns `CLEAR` and confirms
core K-P ownership, content-independent final-pass reachability, Elisp/C
parity, stable ABI, cache ownership, and complete nonempty GUI oracles.
- **Behavior/Risk:** `task034` and its independent developer gate are
complete. `issue021` closed after the user's 2026-08-02 visual
confirmation, and historical `issue018` remains open.

View File

@ -1,33 +0,0 @@
# Change Log: Core Rule Ownership 2026-07-28
## task014
- **Modify**`ekp.el`, `ekp-region.el`
- Centralized the edge-space exclusion used by both Elisp DPs, C-result
reconstruction, and line-glue rendering in one inline pure rule.
- Centralized the five lossless layout marker properties used by renderer
nonstickiness and region logical-string detection.
- **Modify**`tests/ekp-tests.el`
- Added an independent brute-force cross-check for every candidate
line's excluded edge-space width.
- Added a direct marker vocabulary/noninheritance contract.
- **Modify/Add** — developer/audit/changelog records and
`postmortem/20260728-core-rule-ownership.md`
- Recorded why two narrow owners have net value while a file/struct split
does not.
## Validation
- Direct-rule red/green: 0/2 → 2/2; focused related invariants: 4/4.
- Full ERT: 129/129; C/Elisp fuzz: 300/300.
- Warning-as-error production compilation, checkdoc, release, dictionary,
and diff gates: pass.
## Behavior and Risk
- Public behavior and C ABI are unchanged.
- The edge-space helper is `defsubst`; it adds no byte-compiled inner-loop
allocation.
- No wrapper ladder, module, or data object is introduced.

View File

@ -1,57 +0,0 @@
# Change Log: Dictionary Syntax and Provenance 2026-07-28
## Planning
- Added `task011` and a decision gate before parser changes: replacement
syntax could only be implemented if the fixed-width Elisp/C DP and
lossless renderer could represent it correctly.
## task011
- **Modify**`ekp-hyphen.el`, `ekp.el`
- Count and reject slash/replacement dictionaries with a typed,
cached condition.
- Propagate unsupported syntax through the public formatter while still
treating a missing optional dictionary as “hyphenation unavailable.”
- **Add/Modify/Delete**`dictionaries/`
- Added a 49-entry manifest with pinned source paths, SHA-256, syntax
counts, and license evidence.
- Replaced the moving/GNU-specific updater with fail-closed POSIX
`check` and deterministic `export` modes.
- Retained Basque explicitly as a verified legacy byte.
- Removed `hyph_sa_IN.dic`; the pinned upstream snapshot has no
authoritative license statement for that exact hyphenation data.
- **Add** — affected-language and inventory tests
- Locked libhyphen golden expectations for Hungarian, Catalan, and
Albanian, plus the Esperanto slash-pattern boundary.
- Added the offline 49-entry checksum/inventory/license/syntax gate.
- **Modify** — CI, release, public/developer/audit documentation
- Added offline and pinned-upstream dictionary gates.
- Replaced “every bundled dictionary works” with the exact supported
ordinary-pattern contract.
- **Add**`postmortem/20260728-dictionary-contract.md`
- Recorded why parser-only or renderer-only replacement support would be
incorrect and why the future boundary crosses the DP/C ABI.
## Validation
- Affected languages red/green: 0/2 → 2/2.
- Offline manifest: 49/49 pass.
- Pinned upstream check: 49/49 normalized bytes pass.
- Two fresh exports: byte-identical, 49 dictionaries each.
- Full ERT: 121/121.
- Warning-as-error byte compilation: pass.
- Production checkdoc, updater/check shell syntax, workflow YAML, and diff
checks: pass.
## Behavior and Risk
- `eo`, `ca`, `hu_HU`, and `sq_AL` now signal instead of silently using a
linguistically incomplete pattern subset.
- Forty-five ordinary-pattern dictionaries remain directly usable.
- Sanskrit support is deliberately removed until exact license evidence
exists; no compatibility shim or unverified license assumption remains.

View File

@ -1,74 +0,0 @@
# Change: Editing-Frontier Stability 2026-07-29
## 2026-07-29 — Implement and developer-verify task029
- **Modify** `ekp-buffer.el`.
- Adds a source-relative `frontier` marker to live state.
- Derives the active semantic line from the latest real edit, relocates
it in `after-change`, and preserves it through width/font/context
reflow.
- Deletes point-driven live-boundary publication. Point motion inside
the active hard line is display read-only; leaving the hard line keeps
the existing static completion transition.
- Keeps core DP semantics, `ekp.el`, C ABI, schema, plan contract, source
characters, and no-overlay ownership unchanged.
- **Modify** `tests/ekp-buffer-tests.el`,
`tests/ekp-gui-verify.el`, and
`tests/ekp-buffer-live-bench.el`.
- Locks exact property/state/source/undo identity and zero
plan/cache/property writes across backward/forward point motion.
- Proves reflow and deferred IME completion preserve the source-edit
frontier even when point moves elsewhere.
- Updates GUI and benchmark adapters to assert projection, plan,
generation, cache, active-index, and frontier stability.
- **Modify** bilingual user/developer docs, spec, plan, technical
reference, issues, changelog, and superseding postmortem to use the same
source-edit-frontier vocabulary.
- **Verification:** focused regressions RED before implementation and
GREEN afterward; independent focused ERT 10/10; buffer ERT 93/93;
default, seed-`20260729`, and isolated ERT 182/182; C/Elisp fuzz 300/300;
warning-as-error compilation, checkdoc, pinned package-lint, release,
dictionary, pinned-source, no-overlay, no-stale-symbol, and diff gates
pass. Point-motion p99 is 0.033 ms on C and 0.037 ms in the independent
Elisp review, both with zero planner/cache calls.
- **GUI evidence:** reviewed run
`/private/tmp/ekp-frontier-live-v3-66WYRW` contains 39 manifest lines and
a 26.6-second recording. All checkpoints and assertions pass, no black
segment is detected, and the final evidence verdict is PASS.
- **Review:** independent architecture review is CLEAR. Independent code
review reports zero blockers; the one residual performance concern is
separately owned by `issue018`/`task030`.
- **Status:** task029 developer gate complete. `issue016` and `issue017`
remain open until the user personally confirms the visible editing
experience.
## 2026-07-29 — Record the independent append-performance debt
- **Add** `issue018` and `task030` after the current checked-in 80-pixel
benchmark failed to reproduce task028's 6.399 ms append p99.
- **Add** `postmortem/20260729-narrow-live-append-replanning.md` to keep
unique-source-state planning cost separate from task029's point-motion
correctness fix.
- **Evidence:** repeated GC-excluded C-backend runs record roughly
3385 ms append p99, 291 plan calls, and zero cache hits. The latest run
recorded 84.526 ms. In the same run, point motion recorded 0.033 ms p99
with zero plan/cache calls.
- **Behavior/Risk:** documentation only. No performance shortcut or core
DP change is included in task029.
## 2026-07-29 — Plan task029
- **Add** `issue017`: point-only motion currently mutates an already
published semantic live prefix.
- **Add** `task029`: move live-boundary ownership from transient point to
the latest real source edit, while preserving whole-hard-line planning.
- **Modify** the current spec, plan, technical reference, and `issue016`
expectation so point is not a plan or projection invalidation input.
- **Add** `postmortem/20260729-editing-frontier-not-point.md` as the
superseding decision for the point-driven portion of `task028`.
- **Validation planned:** focused RED/GREEN public-hook ERT, complete
buffer/default/random/isolated suites, fuzz/static gates, live
benchmark, GUI dynamic verification, full diff review, and independent
code/architecture review.
- **Status:** Planning complete; superseded by the implementation record
above.

View File

@ -1,53 +0,0 @@
# Change Log: Final Cleanup and Independent Gate 2026-07-28
## task015
- **Delete/Modify** — changed runtime and test files
- Deleted nine unused one-line paragraph accessors and routed remaining
callers directly to the owning `ekp-para` fields.
- Replaced the direct dependency on private
`buffer-substring--filter` with public `filter-buffer-substring`
dispatch under the prior filter binding.
- Kept DELETE lifecycle cleanup outside the temporary dispatch binding so
ownership restoration observes the real buffer-local slot.
- **Review** — fallback-like paths
- Classified language, display, optional-resource, build-command, C
allocation/no-result, sequential-pool, and test-harness paths.
- Retained only documented external-boundary compatibility/fail-safe
behavior with tests and visible failure evidence.
- Found no masking fallback, swallowed business-logic error, broad shim,
or escalation candidate.
- **Modify/Add** — phase, audit, postmortem, and final-gate records
- Documented public filter dispatch ownership in
`postmortem/20260728-public-filter-dispatch.md`.
- Recorded the full post-clean verification and independent review
evidence.
## Validation
- Default and seeded-permuted ERT: 130/130 each.
- Fresh-process isolation: every one of 130 ERT tests passed.
- C/Elisp fuzz: 300/300.
- Production byte compilation with warnings as errors and checkdoc: pass.
- Pinned package-lint commit
`35996f478d81e51dae4fa30d051f741895d07399`: pass.
- Portable, native, debug, and sanitize C profiles: warning-free; portable
artifact restored.
- Release gate, offline dictionary 49/49, pinned-upstream dictionary 49/49,
shell/YAML, diff, private-API, dead-accessor, secret, and Markdown-link
checks: pass.
- Live GUI matrix: 7/7 with a clean fullscreen one-window screenshot.
- Independent code review: 98 paths, zero findings, `APPROVE`.
- Independent architecture invariant review: `CLEAR`.
## Behavior and Risk
- Cleanup reduced indirection and removed a private dependency without
changing public behavior, saved-file formats, C ABI, dependencies, or the
Emacs 29.1 baseline.
- No remote push, tag, publication, release artifact, or production action
was performed.
- Optional product expansions in audit section 4.4 remain demand-gated
future directions, not incomplete repository defects.

View File

@ -1,91 +0,0 @@
# Change: Final-Pass Emergency Stretch 2026-08-02
## 2026-08-02 — Close issue021 after user visual confirmation
- **Modify** the global issue index and issue detail to close `issue021`.
- **User confirmation:** the user accepted the visible result with “可以了,
提交吧” and requested the commit.
- **Behavior/Risk:** no runtime change; this closes the required user-visible
acceptance gate after the developer, dynamic GUI, and independent review
gates had already passed.
## 2026-08-02 — Complete task036 with TeX active-path preservation
- **Modify** `ekp.el` and `ekp_c/ekp_kp.c`: keep strict K-P unchanged; use
fixed `ekp-emergency-stretch-pixel` for ordinary final-pass underfull
candidates; and, only when an overfull candidate would otherwise extinguish
the final active path to a breakpoint, install the best provisional path
with tight fitness and zero incremental demerits.
- **Delete** the wrong-layer rule that forbade an otherwise legal boundary
immediately before an explicit hard atom. Hard atoms forbid only their
interior breaks; an overwide atom remains intact but need not stand alone.
- **Modify** the renderer to distribute the chosen line's actual rest using
its actual TeX glue-set proportions. Elisp 1D, looseness/parshape, and C
behavior match; the public 15-field/15-argument C contract is unchanged.
- **Strengthen tests** so isolated-CJK oracles require a nonempty plan that
exactly covers the source, preventing an empty result from false-greening.
- **Review remediation:** make the changed test fixtures warning-clean under
`byte-compile-error-on-warn` and document the optional eighth
`EMERGENCY-STRETCH` argument in the C README API example.
- **Verification:** focused root regressions 8/8, emergency selector 10/10,
core/buffer/GUI oracle 8/8, full ERT 288/288, seeded and isolated suites
279/279, fuzz 300/300, warning-clean portable/native/debug/sanitize C
builds, warning-as-error production and changed-test Elisp compilation,
checkdoc/package/static/release gates, formal performance gates, and
reviewed 42.78-second dynamic GUI evidence at
`/tmp/ekp-g009-evidence-retry.UOpPNp` with `VERDICT=PASS`. Final independent
code review returns `APPROVE` and architecture review returns `CLEAR`.
- **Behavior/Risk:** no CJK-orphan, unit, atom-adjacency, or screenshot
heuristic exists. `task036` is complete; `issue021` closed after the
user's 2026-08-02 visual confirmation.
The entries below are historical steps. Their hard/atomic fixed-cost wording
was superseded by the completed task036 semantics above.
## 2026-08-02 — Open task036 after task035 full-regression failure
- **Modify** `task035` to record the line-width-sized emergency stretch as a
falsified attempt, not accepted current behavior.
- **Add** `task036` for TeX-style fixed-dimension final-pass emergency
stretch. The implementation must expose `ekp-emergency-stretch-pixel`
where nil auto-resolves to roughly three display-font `M` widths and a
non-negative integer fixes the pixel value.
- **Preserve** the constraints: strict pass unchanged, 15-field C paragraph
ABI unchanged, renderer semantics synchronized, fixed artificial emergency
transition only for truly overfull first permitted hard/atomic runs, and no
CJK-orphan/unit/screenshot-specific penalty.
- **Verification planned:** five new RED cases for fixed dimension behavior,
nil auto, integer override, C parity without ABI growth, and renderer
width/glue consistency; the three regressions broken by task035; focused
G009 GUI/core/public-buffer/C parity; final fullscreen dynamic GUI evidence;
full ERT/fuzz/build/static/release gates.
- **Behavior/Risk:** Documentation records the next atom of work only.
Runtime behavior is not claimed fixed by this entry.
## 2026-08-02 — Close task035 as falsified
- **Modify** core K-P documentation to state the attempted final-pass model:
strict pass unchanged; final pass gives ordinary underfull candidates a
line-width-sized finite background emergency stretch; those candidates
still use adjustment ratio, badness, fitness, and demerits.
- **Modify** user-facing hard-atom documentation to avoid claiming a
hard-coded fullest-prefix fallback. A hard atom may occupy one overflow
line, while the ordinary prefix remains a normal K-P decision with finite
emergency stretch.
- **Modify** `issue021`, `task035`, the spec, plan, changelog, developer
docs, C README, and global change/issue indexes to record the corrected
owner and current acceptance boundary.
- **Add** `postmortem/20260802-final-pass-emergency-stretch.md` to explain
why the ordinary-underfull fixed fallback was the wrong layer, why the
TeX-style emergency-stretch model is used, and why the earlier 84px oracle
was invalid.
- **Verification:** Focused core single-CJK regressions pass 2/2. The
implementation lane reports focused G009 core/C/hard-atom/buffer/GUI-oracle
coverage 18/18, warning-as-error byte compilation pass, and C build pass.
Full regression then falsified the line-width stretch detail and forced the
`task036` fixed-dimension follow-up. This documentation pass ran stale-text
search and `git diff --check`.
- **Behavior/Risk:** `task035` is not current truth. No public configuration,
C ABI, or renderer ownership change is accepted by this record. `issue021`
remains open until task036 and the final user-visible visual acceptance gate
pass.

View File

@ -1,32 +0,0 @@
# Change Log: Fail-Closed GUI Verification 2026-07-28
## task007
- **Modify**`tests/ekp-gui-verify.el`
- Returned structured per-case results.
- Centralized table formatting and pass/fail aggregation.
- Exited with status 1 after reporting any batch failure.
- **Add/Modify** — GUI ERT tests and test loaders
- Added forced-failure and success controls.
- Included the batch-safe GUI boundary tests in normal, permuted, and
isolated runners.
- **Modify** — public, developer, audit, phase, and postmortem records
- Clarified the tests-only loading boundary and nonzero contract.
## Validation
- Focused red: forced-failure control 0/1.
- Focused green: failure/success controls 2/2.
- Default full ERT: 110/110.
- Non-default permuted ERT: 110/110.
- Live GUI matrix: 7/7 PASS, every row `over=0`.
- Screenshot `/tmp/emacs-kp-gui-20260728-clean.png` was inspected: one
fullscreen `*ekp-gui-verify*` window, no split, no scratch or client noise.
## Behavior and Risk
- Interactive matrix behavior remains a visible report buffer.
- Batch callers now receive a trustworthy status; this is intentional.
- The matrix remains a developer tool and adds no runtime dependency.

View File

@ -1,39 +0,0 @@
# Change Log: Hot-loop Performance 2026-07-28
## task012
- **Modify**`ekp-utils.el`
- Accumulate tokenizer box fragments in reverse and concatenate once when
a box is emitted.
- Preserve zero-width attachment and text-property behavior.
- **Modify**`ekp-hyphen.el`
- Distinguish cache misses from cached nil with an uninterned sentinel.
- Build dense inserted output from original word slices in one
concatenation.
- **Modify**`tests/ekp-tests.el`, `tests/ekp-bench.el`
- Add nil call-count, long propertized token, and dense insertion
regressions.
- Add reusable 1k8k adversarial builder measurements.
- **Modify/Add** — developer/audit/issue documentation and
`postmortem/20260728-linear-text-builders.md`
- Record the evidence threshold, measurements, ownership decision, and
rollback boundary.
## Validation
- Nil cache red/green: 0/1 → 1/1; focused behavior tests: 3/3.
- Source-mode 8,000-character tokenizer: 3.133 s → 1.100 s.
- Source-mode 8,000-character dense insertion: 0.945 s → 0.013 s.
- Full ERT: 124/124; C/Elisp fuzz: 300/300.
- Warning-as-error production byte compilation and production checkdoc:
pass.
## Behavior and Risk
- Public tokenization and hyphenation output is unchanged.
- The cache now retains negative results until its existing invalidation
boundary.
- The implementation adds no builder abstraction or dependency.

View File

@ -1,37 +0,0 @@
# Change Log: Composable Buffer Integration Lifecycle 2026-07-28
## task005
- **Modify**`ekp-region.el`
- Saved and restored the previous local/inherited substring-filter owner.
- Composed prior filtering with EKP logical inversion while retaining
DELETE behavior.
- Split structural unjustify from the public lifecycle boundary.
- Removed integrations when the final span disappears outside auto mode,
including external delete paths.
- **Modify**`tests/ekp-region-tests.el`
- Added local/inherited filter restoration, copy and DELETE composition,
final-unjustify cleanup, and auto-mode-disable cases.
- **Modify** — public, developer, audit, and phase documentation
- Documented composition order and lifecycle ownership.
- Closed `issue003` and added
`postmortem/20260728-buffer-integration-ownership.md`.
## Validation
- Focused red: 0/2 for prior-filter output and final-span cleanup.
- Focused green: 2/2; expanded integration matrix 5/5.
- Full ERT: 107/107.
- C/Elisp fuzz: 300/300.
- Byte compilation with warnings as errors: clean.
- checkdoc: clean.
## Behavior and Risk
- Existing mode/user substring filters remain active under EKP.
- Manual unjustify, DELETE, and mode shutdown restore exact ownership.
- A prior filter that deliberately strips all text properties also removes
EKP's structural markers; such a filter owns that returned representation.
- No dependency, public API, or saved-file format changed.

View File

@ -1,36 +0,0 @@
# Change Log: Interactive Protection Workflow 2026-07-28
## task013
- **Modify**`ekp-region.el`
- All four protection commands now share one property/feedback owner.
- Interactive calls report the affected character count and
current-buffer-session lifetime.
- Auto-justify mode help and a standard EKP menu expose the existing
format, protection, removal, and diagnostic commands.
- **Modify**`tests/ekp-region-tests.el`
- Drive no-break and verbatim mark/clear commands through
`call-interactively`.
- Verify behavior through the real public formatters, visible feedback,
mode help, and menu discovery.
- **Modify/Add** — bilingual README, changelog, audit and
`postmortem/20260728-session-local-protection.md`
- State that manual text properties do not survive plain-text save/reopen
and route persistent syntax through faces/predicates.
## Validation
- Focused public-path red/green: 0/3 → 3/3.
- Full default and seeded-permuted ERT: 127/127 each.
- Fresh-process isolation: every one of 127 ERT tests passed.
- Warning-as-error production byte compilation, checkdoc, release,
dictionary, and diff gates: pass.
## Behavior and Risk
- No file format, persistent metadata, or global key binding is added.
- Programmatic callers remain quiet unless they request the optional
announcement; interactive callers always receive feedback.
- Existing commands and arguments remain compatible.

View File

@ -1,28 +0,0 @@
# Change Log: Logical Text API Issue 2026-07-28
## issue011
- **Add**
`.phrase/phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md`
- Recorded that direct Elisp buffer APIs observe EKP's physical layout
representation.
- Captured the verified physical-vs-logical extraction evidence, root
cause, required outcome, and unresolved architecture questions.
- Explicitly deferred implementation and avoided selecting a design or
creating an execution task.
- **Modify**`.phrase/docs/ISSUES.md`, `.phrase/docs/CHANGE.md`
- Added the open issue and linked this documentation-only change.
## Behavior and Risk
- Runtime code and behavior are unchanged.
- No implementation plan, compatibility promise, or solution architecture
was approved.
- The issue remains open until the user chooses to resume design work.
## Validation
- Confirmed `issue011` is the next unused global issue ID.
- Confirmed the issue index link resolves to the phase detail.
- `git diff --check` passes.

View File

@ -1,35 +0,0 @@
# Change: Narrow Live-Append Latency 2026-07-30
## 2026-07-30 — Implement exact incremental live commits
- **Modify** `ekp.el`: retain prepared paragraph and 1D-DP state in layout
plans, retokenize only the last incomplete word plus appended suffix,
extend prefix/break/protrusion data from the first dirty box, resume Elisp
DP from the earliest state that can reach the new tail, and reuse unchanged
layout-line records.
- **Modify** `ekp-buffer.el`: reconstruct only the live dirty island, attempt
exact append planning before a full plan, preserve cache identity, and
release the old hard-line transaction before the next paragraph becomes
live.
- **Modify** `ekp_c/ekp.c`: validate signed 32-bit values with one
`extract_integer` call instead of three Lisp predicate/comparison calls.
- **Add** exact append/fallback, multi-engine, unsafe-context,
stable-boundary, dirty-island, and invalid-C-vector regressions, plus a
frozen baseline/candidate evaluator covering five widths, four paragraph
lengths, C/Elisp, and two GC modes.
- **Performance:** the four-round source-instrumented evaluator reduces
80-pixel C p95/p99 from 114.717/119.201 ms to 25.490/25.785 ms and Elisp
from 588.017/597.093 ms to 43.860/47.578 ms. Ordinary keys remain a
zero-work path at 0.595 ms p99 and all widths avoid regression.
- **Production check:** with production files byte-compiled, three repeated
public-command runs measure append p99 at 1.1581.326 ms for C and
1.4291.438 ms for pure Elisp; hard-boundary p99 is 1.2511.363 ms and
1.4571.470 ms respectively.
- **Decision:** retain the C module. Rust would cross the same Emacs module
ABI and cannot remove the Elisp-owned tokenization, measurement,
transaction, or publication work. Rewriting the already sub-millisecond C
layer would add Cargo and cross-platform release surface without a
measured end-to-end gain.
- **Open gate:** the locked source-instrumented evaluator still exceeds its
absolute 16 ms C/Elisp target, so `task030` and `issue018` remain open even
though the byte-compiled production path is within budget.

View File

@ -1,44 +0,0 @@
# Change: Rigid Inline Atom Break Cascade 2026-08-01
Superseded note: this file records the earlier task033 repair. The current
ordinary-underfull final-pass model is recorded in
`change_final_pass_emergency_stretch_20260802.md`; an overwide atom remains
intact but is not guaranteed a standalone line, and atom adjacency has no
special break rule.
## 2026-08-01 — Implement and developer-verify task033
- **Modify** `ekp.el` so the emergency pass preserves its established first
transition but adds a fullest-underfull prefix fallback only when a
forbidden break run jumps directly to an overfull permitted candidate.
- **Modify** `ekp_c/ekp_kp.c` with the same state transition and keep the
looseness/parshape Elisp path in parity.
- **Add** core, public-buffer, and GUI regressions covering diverse CJK
prefixes, exact source boundaries, atom integrity, Elisp/C parity, and
repeated 480→280→340→280 reflow.
- **Modify** user/developer documentation and add
`postmortem/20260801-forbidden-run-emergency-boundary.md` to record why the
broader fullest-prefix replacement was rejected.
- **Verification:** RED reproduced boundary 1 instead of 11. GREEN passes
normal, random-order, and isolated 201-test runs; 300 C/Elisp fuzz cases;
warning-as-error Elisp compilation; package-lint; checkdoc; release and
dictionary gates; warning-clean debug/sanitize/portable C builds; and
reviewed dynamic GUI evidence at
`/private/tmp/ekp-atom-gui-final-Beg8hb` with verdict PASS.
- **Behavior/Risk:** Strict K-P, ordinary emergency layouts, buffer
projection, source text, and public APIs are unchanged. The additional
relaxation is limited to emergency scans that cross forbidden break
positions before an overfull candidate.
## 2026-08-01 — Plan issue021 and task033
- **Add** `issue021` with the 280px showcase reproduction and deterministic
width-40 batch analogue.
- **Add** `task033` and M12 acceptance gates for the core emergency-break
correction, Elisp/C parity, and real GUI verification.
- **Modify** the text-property layout spec to require a full ordinary prefix
line and one intact rigid atom line rather than one-glyph emergency
fragmentation.
- **Behavior/Risk:** Planning records only. Runtime behavior is unchanged.
The change will affect only the second pass used after strict K-P has
already proved the paragraph unreachable.

View File

@ -1,161 +0,0 @@
# Change: Post-Audit Hardening 2026-08-20
## 2026-09-01 — Restore Emacs 31.1 strict byte compilation
- **Modify** `ekp.el`, `ekp-utils.el`, and `ekp-hyphen.el` by replacing six
obsolete single-binding `if-let`/`when-let` forms with the behavior-identical
starred variants.
- **Modify** `tests/ekp-live-commit-evaluator.el` in the same mechanical way so
maintained performance tooling is strict-compile clean too.
- **Add** `issue029` and close `task044` with exact environment and evidence.
- **Behavior/Risk:** no data flow, branch, public API, layout, hyphenation,
font, module-path, or evaluator behavior changes.
- **Evidence:** root `make`; Emacs 31.1 WERROR production and changed-tool
compilation; normal and seed-20260901 random ERT 296/296; C build; 300-case
parity fuzz; release invariants; local 49-entry dictionary manifest/hash;
checkdoc; and diff-check. The user explicitly excluded the network-backed
fixed-upstream dictionary fetch, so it is not claimed.
## 2026-08-31 — Restore the Emacs 31 fresh-source baseline
- **Modify** `ekp.el` and `ekp-buffer.el` so layout and projection marker
properties update the toplevel default of Emacs 31's automatically
buffer-local `text-property-default-nonsticky`; new and existing buffers
that have no explicit override now inherit the nonsticky protocol.
- **Modify** `tests/ekp-tests.el` and `tests/ekp-buffer-tests.el` so policy
ownership fixtures remain mutable multibyte strings while mutating ASCII
code points that Emacs 31 permits in place. The tests still prove that
cache keys own the original policy values.
- **Behavior/Risk:** no layout algorithm or public policy changes. This fixes
marker noninheritance in real buffers and removes an Emacs 31-only fixture
mutation error.
- **Evidence:** focused ERT 3/3; source-first normal and seed-20260831 random
ERT 296/296; source load, release invariants, 49-entry dictionary manifest,
and pinned dictionary update checks pass.
## 2026-08-20 — Lock follow-up goals and task boundaries
- **Add** `plan_post_audit_hardening_20260820.md` with the evidence-backed
scope, non-goals, acceptance gates, and stop condition for the post-audit
defects.
- **Add** `task_post_audit_hardening_20260820.md` with `task037``task042`
and the remaining `task030` performance gate.
- **Add** issue records for each user-visible or public-contract defect before
implementation, including `issue028` for the stale-bytecode evaluator
false-green.
- **Behavior/Risk:** documentation-only planning. Closed historical issues
remain closed; `issue018` remains open until its locked gate is met.
## Verification
- Next task ID is `task037`; no task ID is reused.
- The plan explicitly separates buffer ownership, live semantics, dictionary
lookup, C validation, test gates, integration lifecycle, and performance.
- No production or test source is changed in this planning pass.
## 2026-08-20 — Implement correctness and gate hardening
- **Modify** `ekp-buffer.el` and `tests/ekp-buffer-tests.el` for preflight
width validation, backward-delete live stability, and no-projection
integration cleanup (`task037`, `task038`, `task042`; `issue022`,
`issue023`, `issue027`).
- **Modify** `ekp-hyphen.el` and `tests/ekp-tests.el` for exact normalized
locale lookup (`task039`, `issue024`).
- **Modify** `ekp_c/ekp.c` and `tests/ekp-c-tests.el` for sorted, unique,
in-range break-position validation in direct and batch APIs (`task040`,
`issue025`), with the 15-field ABI unchanged.
- **Modify** the showcase, source loader, test runners, Windows CI entry,
and complete ERT selection to remove stale-bytecode false greens and
restore automatic inline-code verification (`task041`, `issue026`).
- **Modify** the live evaluator script/bootstrap to load source from the
selected baseline/candidate root (`task030`, `issue028`). The corrected
narrowed matrix still exceeds the source 16 ms target, so `issue018` and
`task030` remain open.
- **Commit:** `6a8c7e0`.
## 2026-08-20 — Record source-fresh performance boundary
- **Add** `postmortem/20260820-source-fresh-live-commit-gate.md` to explain
why the stale-bytecode evaluator result was invalid and why the corrected
source gate remains an open `task030` performance target.
- **Evidence:** width-80/two-row/GC-excluded source candidate p95/p99 are
23.294 ms (C) and 78.593 ms (Elisp), with parity, zero-work, GC,
conflict, and non-regression checks true. The high-row full matrix was
stopped after a source DP cell exceeded two minutes of CPU; it is not
claimed as complete evidence.
- **Commit:** `60b299b` and `postmortem` follow-up commit.
## 2026-08-20 — Attribute incremental append cost and preserve raw evidence
- **Modify** `tests/ekp-live-commit-evaluator.el` to time
`ekp-layout-plan-append` and `ekp--dp-cache-append` separately in each raw
sample and report them beside total/publication timings.
- **Modify** `tests/run-live-commit-evaluator.sh` to write each run into a
unique raw directory and atomically replace the report after comparison.
- **Evidence:** source-fresh width-80/two-row/GC-excluded candidate p95 is
17.161 ms C append / 1.187 ms C append-DP, and 73.293 ms Elisp append /
57.063 ms Elisp append-DP; raw JSONL is nonempty and parity/zero-work/GC/
conflict/non-regression remain true. No production optimization is claimed
yet because the measured DP/append owners require an exact redesign.
- **Commit:** `db6e6b9`.
## 2026-08-20 — Split append preparation, assembly, and DP timings
- **Modify** the evaluator to time `ekp--append-para` and
`ekp--layout-plan-from-para` independently, fixing a wrapper-name collision
found by the first red run.
- **Evidence:** latest source-fresh candidate p95 is C append/append-DP
16.953/1.015 ms and Elisp append/append-DP 71.896/55.970 ms; both engines
spend about 9.753 ms in append preparation and 5.663 ms in plan assembly.
The report remains red, raw JSONL is nonempty, and the wrapper itself now
passes the same matrix without argument errors.
- **Commit:** `01001b9`.
## 2026-08-20 — Implement user-selected native live-append backend
- **Modify** `ekp-buffer.el` and `ekp.el` to let prepared automatic live
append use the loaded C 1D DP when `ekp-auto-justify-native-append` is
non-nil, without changing ordinary full-layout dispatch or the 15-field C
ABI. Native-unavailable and option-disabled paths remain Elisp.
- **Add** `adr_native_live_append_backend_20260820.md`, bilingual README
guidance, evaluator backend metadata, and a public buffer regression proving
the setting toggles the bridge.
- **Verification:** native bridge/parity regression 1/1 and source-first ERT
296/296 pass. In the bounded all-width, 2/4/8/16-row, GC-excluded
source-fresh round, the candidate width-80 C p95/p99 was
14.190/16.495 ms and the Elisp-configured live path (native append backend)
was 13.484/14.334 ms. Layout parity, zero-work, GC, conflict, and width
non-regression checks passed; the C p99 remains just above the locked
16 ms target, so `issue018/task030` stays open.
- **Commit:** `3d3dda6`.
## 2026-08-20 — Close the locked source-fresh live-append gate
- **Verify** the complete four-round evaluator after the native backend change:
every baseline/candidate round loads its own source root across 64/80/96/
128/160 px, 2/4/8/16-row fixtures, C/Elisp engines, and default/excluded
GC.
- **Evidence:** width-80 candidate C p95/p99 is 12.010/14.622 ms and the
Elisp-configured live path (native append backend) is 10.980/11.194 ms;
paired improvements are 77.72/74.03% and 94.19/94.57%. Ordinary-key p99 is
0.551 ms. Layout parity, zero-work, GC exclusion, conflict-free, and
all-width non-regression checks are true. The evaluator then passes full
source-first ERT 296/296, fuzz 300/300, and release checks.
- **Result:** Mark `task030` and `issue018` resolved; retain the raw samples
under `.omx/goals/performance/narrow-live-commit/raw/run.bxULD9` and the
JSON report at `.omx/goals/performance/narrow-live-commit/latest-report.json`.
- **Commit:** this closure-record commit.
## Verification
- Source-first normal ERT: 296/296.
- Source-first seeded random ERT: 296/296, including all nine GUI verifier
tests previously omitted by the name filter.
- Per-test isolated ERT: 294/294 process runs logged `0 unexpected`; the
later batch-position test is covered by focused isolated C ERT.
- C focused ERT: 15/15; C portable build warning-clean; source fuzz:
300/300 with zero failures; checkdoc, release, dictionary, and shell gates
pass.
- Complete source evaluator: parity, zero-work, GC, conflicts, and width
non-regression pass; width-80 C/Elisp p99 is 14.622/11.194 ms. The formal
performance issue is closed.

View File

@ -1,45 +0,0 @@
# Change Log: Property-Sensitive Paragraph Fast Path 2026-07-28
## task016
- **Modify**`ekp.el`
- Store the complete structural paragraph key beside the most-recent
string and paragraph.
- Compare the same key used by the paragraph hash cache before reusing the
fast-path result.
- Delete six style-variable watchers that compensated for the former
partial identity.
- **Modify**`tests/ekp-tests.el`
- Warm one string object, mutate its `ekp-no-break` property in place, and
require the next lookup to match a fresh property-aware paragraph for
CJK and Latin-with-space inputs.
- Retain distinct-string property, language, style, and unchanged-key hit
controls.
- **Modify/Add** — README, developer, audit, issue, phase, and postmortem
records
- Document one cache-identity owner and close `issue010`.
- Record the wrong-layer compensation in
`postmortem/20260728-paragraph-fast-path-identity.md`.
## Validation
- Same-object property mutation red/green: 0/1 → 1/1.
- Focused paragraph-cache matrix: 6/6.
- Default and seeded-permuted full ERT: 130/130 each.
- Fresh-process isolation: every one of 130 ERT tests passed.
- C/Elisp fuzz: 300/300.
- Warning-as-error production compilation, checkdoc, pinned package-lint,
four C profiles, release/dictionary/static gates, and GUI matrix 7/7:
pass.
- Independent architecture review: `CLEAR` for the shared-key ownership.
## Behavior and Risk
- Public APIs, saved-file formats, C ABI, and Emacs 29.1 baseline are
unchanged.
- Cache hits now require the complete text/configuration identity already
owned by `ekp--para-key`.
- The change removes invalidation branches instead of adding watchers or a
second identity model.

View File

@ -1,43 +0,0 @@
# Change Log: Release and CI Governance 2026-07-28
## Planning
- Added `task010` and locked the local/remote boundary, immutable input,
Windows, tracked-record, and version-pair acceptance criteria before code.
## task010
- **Add**`tests/check-release.sh`, `Docs/RELEASING.md`
- Added one executable repository-invariant gate and a distinct
credentialed release checklist.
- **Modify**`.github/workflows/ci.yml`
- Pinned checkout and Emacs setup actions to verified full upstream SHAs.
- Replaced live MELPA installation with a pinned package-lint checkout.
- Added an Emacs 30.1 Windows Elisp compile/ERT baseline.
- **Modify**`.gitignore`, contributor/public/audit documentation
- Made `.phrase` records versionable.
- Documented immutable release artifacts, checksums, and remote checks.
- Closed `issue009` without claiming an unperformed remote publish.
- **Add**`postmortem/20260728-release-governance.md`
- Recorded why local invariants and credentialed remote release evidence
remain separate boundaries.
## Validation
- Release gate red: floating refs, missing Windows/public ERT path, and
ignored `.phrase` were all reported.
- Release gate green: repository invariants pass.
- Pinned package-lint commit: pass with an empty package directory.
- Workflow YAML parse and POSIX shell syntax: pass.
- Default ERT: 119/119.
- `git diff --check`: pass.
## Behavior and Risk
- Full-SHA action pins intentionally require explicit maintenance updates.
- Windows CI covers the required pure-Elisp product baseline; the optional C
module remains exercised on Linux and macOS.
- No commit, tag, remote workflow, artifact, push, or publication was made.

View File

@ -1,39 +0,0 @@
# Change Log: Repository System Audit 2026-07-28
## 2026-07-28
- **Add**`Docs/REPOSITORY_AUDIT_20260728.md`
- Task: `task001`.
- Recorded the architecture baseline, verified strengths, four P1 defects,
seven P2 risks, three P3 opportunities, extension routes, non-goals, and
acceptance gates.
- Separated evidence, inference, and unknowns so future fixes can start
from reproducible facts.
- **Modify**`readme.md`, `readme_zh.md`
- Task: `task001`.
- Corrected the bundled dictionary count, C module version, ERT command
description, GUI verification loading instructions, and the cached
parameter-change caveat.
- Linked the dated repository audit.
- **Modify**`DEVELOPER.md`, `DEVELOPER_ZH.md`
- Task: `task001`.
- Linked the audit and removed stale hard-coded test counts.
- **Modify**`ekp_c/README.md`
- Task: `task001`.
- Corrected the documented malformed-argument behavior without changing
the C API.
- **Add/Modify**`.phrase` plan, task, issue, and index records
- Task: `task001`.
- Registered the audit and its unresolved follow-up work in the current
phase.
## Behavior and Risk
- Runtime behavior is unchanged.
- The report records implementation directions, not approved designs.
- Windows, remote CI, package-lint, sanitizer runtime loading, and a clean
GUI screenshot remain explicit validation gaps.

View File

@ -1,38 +0,0 @@
# Change Log: Non-mutating Save Serialization 2026-07-28
## task004
- **Modify**`ekp-region.el`
- Removed the before/after-save unjustify/rejustify transaction.
- Added one logical write-buffer owner using the documented
`write-region-annotate-functions` buffer-switch contract.
- Added bounded cleanup on success, retry, integration removal, and source
buffer teardown.
- **Modify**`tests/ekp-region-tests.el`
- Added real filesystem failure, forced encoding failure with successful
retry, and `quit` regressions through `save-buffer`.
- Preserved the existing successful logical-write assertion.
- **Modify** — public, developer, audit, and phase documentation
- Documented failure-safe visible behavior and closed `issue002`.
- Added `postmortem/20260728-nonmutating-save-serialization.md`.
## Validation
- Focused red: 0/2; failure and interruption both removed the layout.
- Focused green: 2/2.
- Save matrix: 4/4 for success, filesystem failure, encoding retry, and
interruption.
- Full ERT: 102/102.
- C/Elisp fuzz: 300/300.
- Byte compilation with warnings as errors: clean.
- checkdoc: clean.
## Behavior and Risk
- The display buffer is never unformatted during saving.
- Later write annotations and coding conversion operate on logical text.
- A failed write may retain one hidden logical copy until retry or teardown;
it never grows beyond one per source buffer.
- No global advice, saved-file format, public API, or dependency changed.

View File

@ -1,76 +0,0 @@
# Change Log: Semantic Live Prefix 2026-07-29
## 2026-07-29 — Implement and verify task028
- **Modify** `ekp-buffer.el`.
- Replaces native-row commitment with full-hard-line
`ekp-layout-plan` consumption and point-line semantic prefix
projection.
- Keeps the point-containing line and suffix natural; previous semantic
lines own their break whitespace and may reflow together.
- Adds a 16-entry buffer-local plan LRU, same-line point-motion fast
path, common-prefix differential publication, transactional rollback,
theme-enable/theme-disable/frame-font context invalidation, and
fail-closed oversized/conflict handling.
- Leaves `ekp.el`, core DP semantics, C ABI, DP schema, and plan records
unchanged for this task.
- **Modify** `tests/ekp-buffer-tests.el`,
`tests/ekp-gui-verify.el`, and **Add**
`tests/ekp-buffer-live-bench.el`.
- Covers public editing commands, point boundaries, earlier-break
revision, cache/history reuse, zero-write stable plans, whitespace and
hyphen ownership, IME, failure rollback, resize, narrowing, lifecycle,
exact source, and zero-overlay invariants.
- Default and seed-`20260729` ERT pass 181/181; all 181 tests pass in
independent Emacs processes; C/Elisp fuzz passes 300/300.
- Main dynamic GUI evidence is retained at
`/private/tmp/ekp-semantic-live-v4-vFZTkr`; split-window evidence is at
`/private/tmp/ekp-semantic-split-v3-uPwuOi`. Both reviewed temporal
reports return PASS with exact source, zero overlays, and `hscroll=0`.
- C-backend append p99 is 6.399 ms with GC excluded. Default raw p99 is
50.212 ms when samples include approximately 45 ms GC pauses; this
residual risk is recorded instead of changing global GC behavior.
- **Subsequent audit:** the 6.399 ms result is not reproducible with the
current checked-in 80-pixel benchmark. Repeated GC-excluded task029
runs measure roughly 3385 ms p99 with 291 unique plan misses.
`issue018`/`task030` now track that separate performance debt; this
historical entry is retained to show what the task028 run reported.
- **Modify** bilingual user/developer docs, `CHANGELOG.md`, `issue016`,
plan/technical records, and the superseding postmortem to match the
implemented semantic-prefix model.
- **Behavior/Risk:** The active edit suffix is now native while all earlier
semantic lines are one jointly revisable KP prefix. Automated and GUI
developer gates pass. Independent code review returns APPROVE and
independent architecture review returns CLEAR. `task028` is complete;
`issue016` remains open until the user personally confirms the visible
editing experience.
## 2026-07-29 — Plan task028
- **Add** `issue_semantic_live_prefix_20260729.md`.
- Records `issue016`: live editing currently freezes completed native
rows instead of repeatedly consuming the full hard-line KP plan and
projecting only the semantic prefix before point.
- **Modify** `task_repository_audit_20260728.md`.
- Adds `task028` as the next atomic implementation task with red tests,
performance checks, GUI dynamic verification, and user-confirmation
closure rules.
- **Modify** `spec_text_property_layout_engine_20260729.md`.
- Replaces the native-row live contract with the semantic hard-line
prefix contract: full hard-line plan input, point-line boundary,
prefix-only projection, natural point line and suffix, and no core
DP/C ABI changes.
- **Modify** `plan_text_property_layout_engine_20260729.md`.
- Adds milestone M7 for replacing native-row commitment with semantic
prefix projection while preserving the completed-paragraph DP owner.
- **Modify** `tech-refer_text_property_layout_20260729.md`.
- Supersedes the native progressive row model for live editing and
documents the buffer-local plan cache, line-signature diffing, and
fail-closed ownership rules.
- **Add** `postmortem/20260729-whole-hard-line-live-prefix.md`.
- Records why the native-row model was wrong for global KP alignment and
why the correction belongs in `ekp-buffer`, not in core DP.
- **Behavior/Risk:** Documentation and planning only. Runtime behavior is
unchanged. `issue015` remains open exactly as before pending user-visible
confirmation; `issue016` remains open until implementation and user
confirmation.

View File

@ -1,56 +0,0 @@
# Change: Stable Live Edit Transaction 2026-07-29
## 2026-07-29 — Implement and verify task031
- **Modify** `ekp-buffer.el`: replace frontier-owned immediate publication
with committed live state plus one baseline-snapshot edit transaction;
naturalize only the owning dirty span range; restore reversible edits
exactly; and publish only at native-row or structural commit events.
- **Modify** `ekp-buffer.el`: keep active live spans out of static lazy-reflow
chunks and recognize both forward and backward native-row crossings.
- **Modify** `tests/ekp-buffer-tests.el` and
`tests/ekp-gui-verify.el`: add RED/GREEN public-path coverage for zero-plan
same-row edits, local middle-row anchors, object-identical reversal,
forward/backward row crossing, zero-work point motion, lazy ownership,
stable yank/undo, resize, and hard completion.
- **Modify** bilingual user/developer documentation, changelog, spec, plan,
technical reference, task/issue records, and the decision postmortem to
make transaction triggers and signature-diff ownership explicit.
- **Verification:** buffer 99/99; default, seed-`20260729`, and isolated ERT
188/188 each; C/Elisp fuzz 300/300; warnings-as-errors production/test
compile; empty checkdoc; pinned package-lint exit 0; release and 49-entry
dictionary manifest pass. The earlier exact pinned-source run remains
applicable because dictionary bytes did not change; two redundant final
fetches failed with GitHub `early EOF`, not a byte mismatch.
- **Benchmark:** with GC excluded, cache-revisit and point-motion scenarios
perform zero plans; 291 appends contain 15 structural plans. Remaining
C/Elisp structural p99 spikes move to `issue018`/`task030`.
- **GUI evidence:** reviewed PASS at
`/private/tmp/ekp-stable-transaction-final5-2BFryc`: 48.95 seconds,
55 checkpoints, 17 assertions per checkpoint, completed run-end, one
fullscreen target window, no black segment, split, stale buffer, client
instruction, or transient blank frame.
- **Behavior/Risk:** ordinary editing is intentionally stability-first;
the current dirty row may be temporarily non-optimal until a structural
commit. Completed paragraphs still use the unchanged global KP core.
## 2026-07-29 — Plan issue019 and task031
- **Add** `issue019`: per-edit whole-hard-line replanning and suffix
invalidation share one overloaded frontier and cannot express stable local
editing or exact reversible restoration.
- **Add** `task031`: replace that model with committed projection, a dirty
edit transaction, preserved unaffected anchors, and atomic structural
commits.
- **Modify** `task030` to depend on `task031`; performance work must measure
the surviving path instead of optimizing the rejected control flow.
- **Modify** the current spec, plan, technical reference, issue index, and
decision history so trigger ownership is explicit: transactions decide
when layout may change, signature diffing decides what properties change,
and the unchanged core DP decides final layout.
- **Validation planned:** focused public-path RED/GREEN ERT; complete
automated/static gates; re-profiled live benchmark; temporal GUI evidence
for stable input, local middle-line editing, exact reversible restoration,
structural commits, and zero-work point motion.
- **Behavior/Risk:** this planning entry is superseded by the completed
implementation entry above.

View File

@ -1,31 +0,0 @@
# Change Log: ERT State and Order Isolation 2026-07-28
## task006
- **Modify**`tests/ekp-tests.el`
- Dynamically scoped and restored all isolated EKP tunables.
- Replaced the mislabeled internal predicate assertion with a public
parshape dispatch test and added a fixture-restoration control.
- **Add/Modify** — test runners and `.github/workflows/ci.yml`
- Added seeded permutation and fresh-process-per-test runners using public
ERT APIs.
- Added the reproducible permuted-order lane to CI.
- **Modify** — changelog, developer, audit, phase, and postmortem records
- Documented the commands, invariant, evidence, and `issue004` closure.
## Validation
- Focused red: isolated C-loaded parshape test 0/1.
- Focused green: public dispatch, fixture restoration, and C-parameter
cases 3/3; isolated parshape 1/1.
- Reproducibly permuted full ERT: 108/108.
- Every ERT in a fresh Emacs process: 108/108.
## Behavior and Risk
- Production code and public API are unchanged.
- CI gains one full ERT invocation on Emacs 30.1.
- The slower fresh-process runner is an explicit local/release diagnostic,
not part of every CI job.

View File

@ -1,286 +0,0 @@
# Change Log: Text-Property Layout Implementation 2026-07-29
## 2026-07-29 — Complete task027
- **Modify** `ekp-buffer.el`.
- Makes native soft wrapping a mode-owned precondition by temporarily
setting both `truncate-lines` and
`truncate-partial-width-windows` buffer-locally to nil before the
first reflow.
- Restores exact prior values and local/global ownership on normal
disable, major-mode teardown, and failed activation. An activation
error rolls back the full lifecycle and then remains visible.
- Centralizes installation and removal of the auto-mode hook set; no
source character, overlay, synthetic live break, or wrapping algorithm
was added.
- **Modify** buffer/GUI tests and bilingual user/developer documentation.
- Adds public lifecycle regressions for normal disable, forced reflow
failure, and major-mode teardown.
- Adds a deterministic 44-column split-window scenario and records the
native-wrap requirement in the spec, plan, issue, changelog, and
decision history.
- **Validation:** focused lifecycle ERT 3/3; buffer ERT 77/77; default,
seed-`20260729`, and isolated full ERT 166/166; C/Elisp fuzz 300/300;
warning-as-error compilation; checkdoc; pinned package-lint; release,
dictionary, pinned-source, no-overlay, stale-name, conflict, artifact,
and diff gates.
- **Dynamic evidence:** `/private/tmp/ekp-soft-wrap-final-pass-PIUigY`
retains a reviewed per-character Emacs 30.2 recording in an intentional
two-window frame. The 44-column selected window advances from one to two
visual rows with `hscroll=0`, exact source text, zero overlays, zero live
replacing breaks, and no pending edit. Manifest/report/recording/contact
SHA-256:
`53cd23f8e1be1f29545edb6daa80184d438ca87d4d7068a182faf691a8437ad8`,
`a97a74cc5a6255c2eb5992c266c727e3ed5d5556f37672ca319373a65a110e4d`,
`2b3f7c155d1f0e2feeca2bb79a86aafe612cc1489c486121491ec6268bc1de0a`,
`21a78ae849e3a1d5cda1d80ee375cd655e49b221b5192a219ac6ab11d9122d53`.
- **Behavior/Risk:** Narrow split windows now follow normal Emacs soft
wrapping while auto mode is active. A buffer that deliberately requested
truncation gets that exact setting back when the mode ends. `issue015`
stays open pending user-visible confirmation.
## 2026-07-29 — Plan task027
- **Add** `issue_native_soft_wrap_20260729.md`.
- Records `issue015`: Emacs's default narrow partial-window policy
horizontally truncates the active paragraph, so the native progressive
engine never observes a completed visual row.
- **Modify** phase spec, task list, and global issue/change indexes.
- Makes native soft wrapping an explicit auto-mode lifecycle
precondition with exact restoration on teardown.
- **Behavior/Risk:** Planning and red-test preparation only. Runtime
behavior is unchanged at this entry.
## 2026-07-29 — Complete task025 and task026
- **Modify** `ekp-buffer.el`.
- Deletes partial-KP live lookahead, stable anchors, line signatures,
convergence, and push/pull repair.
- Makes Emacs native redisplay own the active tail. EKP projects only
internal gap widths on completed native visual rows and reserves full
KP breaks, prefixes, and discretionary hyphens for completed hard
paragraphs.
- Invalidates a committed prefix before edits cross back into it, defers
IME composition, rejects stale generations, respects narrowing, and
keeps foreign display ownership isolated.
- **Modify** `ekp.el`, buffer tests, and GUI verification.
- Removes the now-unused partial live-flow API and replaces its
implementation-shaped tests with the approved three-state behavioral
contract.
- Adds dynamic actions for native wrap, deletion restoration, and hard
paragraph completion.
- **Modify** bilingual user/developer docs, phase spec/plan/tasks/issue,
changelog, and the live-layout decision history.
- Records the native progressive model as the current workflow without
rewriting the earlier partial-flow decision as if it never existed.
- **Validation:** focused live contract ERT 4/4; buffer ERT 75/75; default,
seed-`20260729`, and isolated ERT 164/164; fuzz 300/300;
warning-as-error compilation; checkdoc; pinned package-lint; release,
dictionary, pinned-source, ownership, no-overlay, stale-name, and diff
gates. Exact 164px ASCII/CJK probes and all seven GUI variants pass.
- **Dynamic evidence:** `/private/tmp/ekp-native-live-2dYu2U` retains a
14.82-second fullscreen single-window Emacs 30.2 recording. It proves
native mixed-text wrap with zero live owned breaks, exact deletion back
to the original three native rows, and full KP only after hard newline.
Every immediate/redisplay assertion passes; the recording contains no
black segment or non-Emacs frame. Manifest/report/recording SHA-256:
`cc3a626c7f8fb68f1ef1ea4ba0dd1e276281afee7a56fb88835f49bd86f239e1`,
`0be9337a95469c6fe710518dbfa27432b4abcd511f623182021776f911887ab0`,
`f87c3adbf16e5d17860471c1a85c89dc688b24c8e08ad82172291fc87483562e`.
- **Behavior/Risk:** Underfilled editing performs no layout planning and
looks native. Completed native rows receive gap-only alignment. A hard
paragraph receives full KP only after completion or exit. Native visual
row discovery still depends on one authoritative graphical window;
alternate-width simultaneous plans remain intentionally unsupported.
`issue014` stays open pending user-visible confirmation.
## 2026-07-29 — Plan task025 and task026
- **Add** `issue_natural_live_editing_20260729.md`.
- Records `issue014`: partial KP breaks and hyphens appear while typing
near the right edge.
- **Modify** the phase spec, plan, task list, and global issue/change
indexes.
- Replaces the live product contract with the approved three-state model:
native active tail, gap-only completed native rows, and full KP after
hard-paragraph completion.
- **Behavior/Risk:** Planning and red-test preparation only. Runtime
behavior is unchanged at this entry.
## 2026-07-29 — Complete task023 and task024
- **Modify** `ekp-buffer.el`.
- Restores the mark marker without activating it, then restores
`mark-active` as independent editor state.
- Keeps static paragraph-edge whitespace hiding off the active live line
and clears from the source span beginning when its first line becomes
active.
- **Modify** buffer tests and GUI verification evidence.
- Adds inactive/active mark, same-turn leading/trailing space and tab,
CJK space, deletion-exposed whitespace, consecutive space/backspace,
yank, newline, and undo regressions through public command paths.
- Retains the existing overflow/pullback, paragraph transition, resize,
IME, foreign-owner, teardown, and bounded-work matrix.
- **Modify** bilingual user/developer docs, phase spec/plan/tasks/issues,
changelog, and `postmortem/20260729-active-line-edge-state.md`.
- Records the editor-state and active-line ownership rules at their
durable documentation boundaries.
- **Validation:** focused red 0/3 → green 3/3; buffer ERT 79/79; full
default and seed-`20260729` ERT 170/170; isolated fresh-process ERT
170/170; C/Elisp fuzz 300/300; warning-as-error compilation; checkdoc;
pinned package-lint; release, dictionary, shell, artifact, stale-name,
no-overlay, and diff gates.
- **Dynamic evidence:**
`/private/tmp/ekp-interaction-clean-rdZhdD` records three actions:
showcase width change, one-space insertion, and deletion exposing that
space. Every immediate and redisplay assertion passes, with no region,
pending transaction, replacing display, overlay, black segment, or
non-Emacs window. Manifest/report/recording SHA-256:
`0eaeebd1697d69304cfd70189d7381a026b761333ab46ab1e2c5923a6e2db0c9`,
`940efdb923a831dcd4b098cafee33b9a67c472f227be1897bd54cab0693f8b68`,
`519a4ba2757b453e54a2518c83784bfed26317eae4c3e246a984f38a2d30b2f5`.
- **Behavior/Risk:** The two regressions are fixed at shared projection
owners rather than showcase keys or self-insert advice. Static committed
lines retain edge cleanup. `issue012` and `issue013` remain open only for
required user-visible acceptance.
## 2026-07-29 — Plan task023 and task024
- **Add** `issue_live_editing_interaction_20260729.md`.
- Records `issue012` for inactive-mark activation during reprojection and
`issue013` for hidden active-line edge whitespace.
- **Modify** the phase spec, plan, task list, and global issue/change
indexes.
- Makes same-turn whitespace visibility and independent `mark-active`
preservation explicit product contracts.
- Adds a bounded adjacent-interaction audit instead of treating the two
reported symptoms as isolated key-handler patches.
- **Behavior/Risk:** Planning and red-test records only; runtime behavior is
unchanged at this entry.
## 2026-07-29 — Complete task022
- **Add** `spec_text_property_layout_engine_20260729.md`.
- Records the current user-visible contract, command and live-edit flows,
display-property mapping, failure/rollback behavior, edge cases, and
acceptance criteria.
- Corrects a completion-audit gap: the objective required a phase spec,
but the implementation phase had only plan/technical records.
- **Modify** implementation, tests, automation, bilingual documentation,
phase records, changelog, and the live-layout postmortem.
- Links the spec from the plan, tasks, issue, and change trace.
- Closes the repository-wide text-property architecture gate after a full
diff and ownership review.
- Removes the dead `ekp-buffer--justified-spans` and
`ekp--para-glue-shrink` functions; adds no wrapper, compatibility shim,
dependency, overlay path, or source-character fallback.
- **Validation:** buffer ERT 71/71; full default and seeded-permuted ERT
162/162; isolated fresh-process ERT 162/162; C/Elisp fuzz 300/300;
warning-as-error byte compilation; checkdoc; pinned package-lint;
release, dictionary, pinned-upstream dictionary, shell, diff, stale-name,
no-overlay, and dead-private-function scans; portable/native/debug/
sanitizer C builds; exact 164px GUI glue; seven-case GUI matrix; and
dynamic overflow/type/delete temporal verification.
- **Retained GUI/dynamic evidence:**
- `/tmp/ekp-live-final-029c-fOG8xp/manifest.jsonl`
(`sha256:727305251edf07e4cee43bc6b8b25610a384e7d4f071656de388ec6e23913fd9`)
records one live Emacs window, source/projection hashes, zero overlays,
exact overflow/pullback assertions, immediate/redisplay equality, and a
completed 31,707ms run.
- The clean first and final screenshots are
`frames/000001-setup-setup.png`
(`sha256:5e324e25f278867b57c31b825c7d56c87d15b18f7fd1e99e276857127c0ddab8`)
and `frames/000008-completion-final.png`
(`sha256:f5e28d98190b8a494d7f010b423c217b104e76f8c79cc792a8359a328ae1797d`).
- `report.md`
(`sha256:fec75f10a851f220a937456fa17900c178f9466874340d1e178ca6e333693904`)
returns `PASS` with no incomplete or black segments; `recording.mov`
(`sha256:2e31a13b70c2387a0e9a08a1b87947180a46310e3f1becb8c445c768998ac347`)
is the 4,875,735-byte temporal source.
- **Independent review:** code reviewer `APPROVE`, zero findings;
architecture reviewer `CLEAR`, no unresolved architectural blocker.
- **Evidence correction:** replaces the stale duration in phase records
with 31.71 seconds, matching the retained dynamic
manifest's 31,707ms `run-end`.
- **Strengthen** `tests/ekp-buffer-tests.el`.
- Replaces presence-only display checks with exact structural assertions
for ASCII `((space-width FACTOR) (min-width ((PIXELS))))`, zero-source
CJK `(min-width ((PIXELS)))`, and the `line-prefix` pixel-space value.
- Focused exact-property ERT passed 3/3; the current full suite passed
162/162. Warning-as-error compilation of the strengthened test file,
production checkdoc, release checks, artifact cleanliness, and
`git diff --check` passed.
- **Behavior/Risk:** The runtime and repository gates are complete.
`issue011` intentionally remains open until the user accepts the visible
editing behavior, as required by the issue lifecycle.
## 2026-07-29 — Complete task021
- **Modify** `ekp-buffer.el`.
- Adds synchronous bounded live flow with a natural unfinished line,
minimal overflow push, deletion pullback, stable-line convergence,
generation-checked IME/resize work, and automatic paragraph limits.
- Keeps one source-marker anchor for a continuous editing flow so
repeated deletion cannot move the recomputation boundary into the
stable prefix.
- Releases the active anchor on paragraph exit, explicit refill,
hard-boundary reflow, conflict abandonment, and mode teardown.
- **Modify** `tests/ekp-buffer-tests.el`, `tests/ekp-gui-verify.el`.
- Adds public-hook live-edit regressions, bounded-work tests, exact
type/delete break restoration, anchor lifecycle checks, and
before/immediate/settled source/projection hash assertions.
- **Modify** bilingual user/developer docs, current plan/test specification,
changelog, and `postmortem/20260729-text-property-live-layout.md`.
- **Behavior/Risk:** Ordinary edits never run a delayed whole-paragraph
formatter. The unfinished point line remains natural; overflow and
pullback publish synchronously. Buffer ERT passed 71/71. The final
31.71s dynamic artifact passed every checkpoint with zero overlays,
no immediate/settled hash drift, and exact deletion restoration.
## 2026-07-29 — Complete task020
- **Delete** `ekp-region.el`, `tests/ekp-region-tests.el`.
- **Add** `ekp-buffer.el`, `tests/ekp-buffer-tests.el`.
- Replaces delete/insert physical layout with owned text properties on
existing source characters only.
- Maps ASCII glue to combined `space-width`/absolute-pixel `min-width`,
zero-source glue to `min-width`, indentation to `line-prefix`, and
breaks/hyphens to replacing display strings.
- Preserves foreign property owners, direct logical text APIs, copy/save/
search semantics, point, undo, modified state, and external hooks.
- **Modify** GUI verification and bilingual user/developer documentation.
- **Behavior/Risk:** Buffer ERT passed 71/71. The clean Emacs 30.2 static
probe passed exact 164px ASCII/CJK targets, display-only break/hyphen,
point/vertical motion/region/mouse, unchanged source, and zero overlays;
the 7-case scale/remap/fringe/width matrix passed.
## 2026-07-29 — Complete task019
- **Modify** `ekp.el`.
- Adds semantic `ekp-layout-plan`, `ekp-layout-line`, and
`ekp-layout-gap` records with source offsets, exact glue targets,
break kinds, and discretionary-hyphen decisions.
- Makes the existing reversible string renderer consume the shared plan.
- **Modify** `tests/ekp-tests.el`.
- Adds source mapping, hyphen decision, and public-dispatch regressions.
- **Modify** `DEVELOPER.md`, `DEVELOPER_ZH.md`.
- Documents the layout/rendering representation boundary.
- **Behavior/Risk:** Public string formatting remains compatible. The new
plan is the single source for the upcoming buffer display projection.
Validation: focused red 0/3 → green 3/3; full ERT 133/133; fuzz 300/300;
warning-as-error compilation, checkdoc, and diff checks clean.
## 2026-07-29 — Plan task019 through task022
- **Add**
`plan_text_property_layout_engine_20260729.md`.
- Locks the shared core-plan architecture, text-property-only buffer
mapping, live active-line state machine, narrowest-window policy,
display ownership, verification gates, and rollback boundary.
- **Modify**
`task_repository_audit_20260728.md`.
- Adds atomic `task019` through `task022` for core extraction, static
projection, live editing, and final closure.
- **Behavior/Risk:** Planning records only. Runtime behavior is unchanged.
The main risk is Emacs display-property interaction; each milestone has a
stop gate before downstream implementation.

View File

@ -1,56 +0,0 @@
# Change Log: Text-Property Layout Research 2026-07-29
## task018 / issue011
- **Add**
`.phrase/phases/phase-kp-overhaul-20260726/tech-refer_text_property_layout_20260729.md`
- Recorded the user-mandated text-property-only direction.
- Mapped KP boxes, Latin/CJK glue, indentation, chosen breaks, and
discretionary hyphens to Emacs display properties.
- Recorded official documentation/source evidence, exact boundaries, and
a minimum GUI verification gate.
- Proposed a non-polluting Latin hyphen representation in which the
preceding source grapheme displays as
`GRAPHEME + HYPHEN + NEWLINE`.
- **Modify**
`.phrase/phases/phase-kp-overhaul-20260726/issue_logical_text_api_20260728.md`
- Marked design research as resumed while keeping the issue open.
- Added the confirmed primitives, Emacs 30.2 GUI probe results, proposed
hyphen owner, and unresolved architecture constraints.
- **Modify**
`.phrase/phases/phase-kp-overhaul-20260726/task_repository_audit_20260728.md`
- Added and completed documentation-only `task018`.
- **Modify**`.omx/plans/prd-ekp-seamless-live-layout.md`,
`.omx/plans/test-spec-ekp-seamless-live-layout.md`
- Added a supersession notice so the prior overlay-oriented prototypes
are not executed after the user's text-property-only decision.
- **Modify**`.phrase/docs/CHANGE.md`
- Indexed this phase change.
## Behavior and Risk
- No runtime source, test implementation, package API, or persisted file
format changed.
- The recorded GUI probes used temporary buffers in clean Emacs 30.2
daemons and left no repository or user-buffer state.
- Static LTR feasibility is evidence, not proof of live editing,
multi-window independence, bidi correctness, or foreign display-property
composition.
- `issue011` remains open.
## Validation
- Official GNU Emacs display/text-property documentation was cross-checked
against the Emacs 30.2 display source.
- GUI glue probe: a natural 7px source space rendered at exact 3px and 20px
targets through combined `space-width` and `min-width`; source characters
were unchanged.
- GUI hyphen probe: `abcdefgh` remained eight source characters while a
property on `d` displayed `d-\n`, produced two screen lines, moved one
visual line to source position 5, and honored a 20px `line-prefix`.
- Documentation links, task/change traceability, Markdown structure, and
`git diff --check` were inspected.

View File

@ -1,23 +0,0 @@
# Issue: Buffer Width Validation and Failure Atomicity 2026-08-20
## issue022 [ ] Invalid buffer widths mutate or create invalid projection state
- **Status:** Runtime fix verified by `task037`; user-visible confirmation
remains pending.
- **Summary:** `ekp-justify-region` accepts zero, negative, and non-integer
widths without the string API's positive-integer validation. It clears the
existing projection before a later type error, so a rejected request can
destroy valid visible state.
- **Repro:** In a justified buffer, call `ekp-justify-region` with `0`, `-1`,
`1.5`, or `"80"`.
- **Expected vs Actual:** Expected a preflight signal with the old projection
unchanged. Actual zero creates a span with width zero; a type error after a
prior projection leaves no spans.
- **Related:** `ekp-buffer.el`, `ekp.el`, `task037`.
- **Fix:** `ekp-justify-region` now validates the width before font-lock or
projection clearing.
- **Verification:** Red/green focused ERT 1/1 and source-first full ERT
295/295.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,19 +0,0 @@
# Issue: C Position Vector Validation 2026-08-20
## issue025 [ ] C direct API accepts invalid break positions
- **Status:** Closed by direct contract verification on 2026-08-20.
- **Summary:** The C boundary validates vector shape and signed integer type,
but not the exclusive break-index range, monotonic ordering, or duplicate
policy of hyphen/forbidden positions.
- **Expected vs Actual:** Malformed direct arguments should signal
`ekp-c-invalid-input` per the C API contract. Current `[-1]` and `[99]`
vectors return a layout result instead.
- **Related:** `ekp_c/ekp.c`, `ekp_c/ekp_kp.c`, `task040`.
- **Fix:** Shared preflight now rejects out-of-range, duplicate, and
non-increasing vectors for both direct and batch calls.
- **Verification:** Direct/batch C ERT 15/15, portable warning-clean build,
300-case fuzz, and Elisp parity pass.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer implementation and contract verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,56 +0,0 @@
# Issue: C Resize Latency 2026-07-29
## issue020 [x] C-backed resize reflow still takes roughly 6070 ms
- **Status:** Closed by user re-audit on 2026-08-13. Developer verification
was complete.
- **Summary:** Resizing with the C dynamic module enabled was observed at
roughly 6070 ms. That number covered more than the C algorithm: the
buffer path also rebuilt layout input, crossed the Emacs-module boundary,
reconstructed layout data, and published text properties.
- **Environment:** Graphical Emacs 30.2 on Apple Silicon; C module 1.6;
portable production profile unless a benchmark explicitly labels another
profile.
- **Repro:**
1. Load the C module and a representative mixed Latin/CJK buffer.
2. Reflow the same source through a deterministic sequence of uncached
widths.
3. Measure complete reflow and its plan, module, DP, and publication
layers with GC pauses reported separately.
- **Expected vs Actual:**
- Expected: candidate p95 is at most 50 ms and both p50 and p95 improve by
at least 20% against the frozen same-machine baseline.
- Actual: the final four-round evaluator records core p50/p95 of
15.318/27.687 ms and complete resize p50/p95 of 15.900/27.487 ms.
Against the frozen baseline this is a 33.25%/34.09% core improvement
and a 43.51%/41.03% complete-resize improvement.
- **Investigation:** The portable C call averaged roughly 1.8 ms and was
not the dominant layer. Repeated paragraph resolution, projection-time
gap measurement, no-op gap records, duplicate property publication, and
publishing the active paragraph once statically and again as the live
prefix owned most avoidable work. A real GUI resize can still include an
Emacs GC pause after `set-window-margins`; that pause is reported
separately instead of being hidden by a global threshold change.
- **Required Outcome:** Reduce the real owning layer while preserving exact
frozen-C and Elisp layout parity, exact cache identity, stable live
transactions, and source-clean projection.
- **Forbidden Shortcuts:** Longer debounce, skipped intermediate widths,
stale or approximate plan reuse, asynchronous stale publication, and
global GC changes.
- **Fix:** Resolve each paragraph once per plan, carry prepared DP/gap
geometry through projection, omit true zero-source/zero-target gaps,
publish owned properties atomically, and exclude the active paragraph
from the static resize pass before installing its live prefix.
- **Verification:** Four interleaved frozen-baseline/candidate rounds pass
the core and complete-resize gates with exact frozen-C/Elisp layout
parity. The normal and permuted 193-test suites pass, 300 fuzz cases pass,
warning-as-error byte compilation, package-lint, checkdoc, release checks,
and the isolated-test run pass. Reviewed GUI evidence at
`/private/tmp/ekp-c-resize-gui-final6-FMFLxx` records four real width
changes, 14 checkpoints, no failed assertion or pixel overflow, and a
maximum EKP mutator time of 22.989 ms; one 74.099 ms total sample contains
58.792 ms of Emacs GC and 15.307 ms of EKP work.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; developer verification complete.
- **Commit:** — (documentation-only closure).

View File

@ -1,44 +0,0 @@
# issue029 — Emacs 31.1 strict compilation rejects obsolete let macros
Status: resolved
## Environment
- GNU Emacs 31.1 development build `fac653279dcb`
- macOS Apple Silicon
- EKP HEAD `97cb6a6e3506`
## Reproduction
Run the repository CI byte-compilation command with
`byte-compile-error-on-warn` set to `t` against the four production files.
## Expected vs actual
- Expected: production and maintained test tools compile with warnings treated
as errors on the recorded Emacs executable.
- Actual: Emacs 31.1 rejects six single-binding `if-let`/`when-let` forms in
production; strict changed-tool compilation also finds three such forms in
`tests/ekp-live-commit-evaluator.el`.
## Investigation and fix
The macros became obsolete in Emacs 31.1. Every affected form has exactly one
binding, so the starred replacement has identical binding, truth, body, and
fallback behavior and needs no compatibility branch. Replace only those nine
macro names; do not alter layout or evaluator data flow.
## Verification
`task044` passed root integration compilation; WERROR production and changed
evaluator compilation; normal and seed-20260901 random ERT 296/296; warning-clean
C build; 300-case C/Elisp parity fuzz; release invariants; local 49-entry
dictionary manifest/hash; checkdoc; and diff-check. The network-backed
fixed-upstream dictionary fetch was stopped and excluded at the user's explicit
direction; no upstream-provenance claim is made.
Resolved At: 2026-09-01
Resolved By: C1a nine-repository verification
Commit: pending (protected pre-existing dirty worktree)

View File

@ -1,20 +0,0 @@
# Issue: Hyphenation Locale Resolution 2026-08-20
## issue024 [ ] Hyphenation accepts common locale spelling but selects the wrong dictionary
- **Status:** Runtime fix verified by `task039`; user-visible confirmation
remains pending.
- **Summary:** `de-CH` and `de-DE` normalize to underscore forms but are
looked up in the short-code registry, where they can resolve to the first
arbitrary German dictionary rather than the exact locale.
- **Expected vs Actual:** Equivalent `de_CH`/`de-CH` spellings should select
`hyph_de_CH.dic`; current `de-CH` selects `hyph_de_AT.dic`.
- **Related:** `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`,
`task039`.
- **Fix:** Raw and normalized exact locale keys are checked before the
documented short-code fallback.
- **Verification:** Locale-equivalence ERT 11/11 and full dictionary gates
pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,20 +0,0 @@
# Issue: No-Projection Integration Lifecycle 2026-08-20
## issue027 [ ] Manual integrations remain installed with no projection owner
- **Status:** Closed by lifecycle verification on 2026-08-20.
- **Summary:** A manual justify request that produces no spans, such as an
empty or foreign-display-only buffer, still installs EKP change hooks and
the substring filter even though auto-mode is disabled.
- **Expected vs Actual:** Integrations should have one active owner: spans or
auto-mode. Actual `spans=nil` still leaves the filter and hooks installed
until a later change happens to trigger cleanup.
- **Related:** `ekp-buffer.el`, `plan_integration_lifecycle_20260728.md`,
`task042`.
- **Fix:** Manual justification installs integrations only when a span or
auto-mode owner exists.
- **Verification:** Empty/foreign-only lifecycle ERT 2/2 and full buffer
suite pass.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer implementation and lifecycle verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,22 +0,0 @@
# Issue: Backward Live Projection Stability 2026-08-20
## issue023 [ ] Backward deletion can republish a stable projection
- **Status:** Runtime fix verified by `task038`; user-visible confirmation
remains pending.
- **Summary:** Live row crossing treats forward typing and backward deletion
identically. After a forward crossing, deleting a suffix into the previous
native row can republish earlier rows and visibly move already typed text.
- **Expected vs Actual:** The current live-edit contract preserves unaffected
committed projection anchors during local tail shrink. Actual
`ekp-buffer--live-row-crossed-p` triggers a full prefix publication in both
directions.
- **Related:** `postmortem/20260730-incremental-live-append-ownership.md`,
`ekp-buffer.el`, `task038`.
- **Fix:** A live row crossing now commits only when the native row moves
forward; backward tail shrink keeps the transaction local.
- **Verification:** Public edit-path regression and source-first full/random/
isolated ERT all pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,119 +0,0 @@
# Issue: Live Editing Interaction Regressions 2026-07-29
## issue012 [x] Reprojection activates an inactive mark
- **Status:** Closed by user re-audit on 2026-08-13. Runtime fix and
repository verification were complete.
- **Summary:** Changing width in `ekp-showcase` can highlight text even
though the user did not activate a region.
- **Environment:** Text-property renderer after `task022`; Emacs 30.2.
- **Repro:**
1. Open `ekp-showcase`.
2. Move point so the buffer has an existing but inactive mark.
3. Press `-`, `+`, Left, or Right to change the projection width.
4. Observe a highlighted region between point and the old mark.
- **Expected vs Actual:**
- Expected: width changes preserve point, mark position, and whether the
region is active.
- Actual: the old mark becomes active during reprojection.
- **Investigation:** `ekp-justify-region` restores every existing mark with
`set-mark`. That command activates the mark even when it was inactive
before layout. The showcase exposes the central command defect because
every width key calls the same reprojection path.
- **Root Cause:** The projection command restores the mark's position but
does not preserve the independent `mark-active` editor state.
- **Required Outcome:** Restore the mark marker without activating it, and
prove both the central command and showcase width flow leave an inactive
mark inactive.
- **Fix:** `task023` now restores the existing mark marker directly and
restores `mark-active` independently. It never calls `set-mark` during
reprojection.
- **Verification:** The focused regression failed before the fix because
`mark-active` became non-nil, then passed after the owner-layer change.
A clean full-screen GUI width-key run preserved point 120, mark 20,
`mark_active=false`, `region_active=false`, and zero overlays before,
immediately after, and after redisplay.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; runtime work previously implemented.
- **Commit:** — (documentation-only closure).
## issue013 [x] Active-line edge whitespace is hidden
- **Status:** Closed by user re-audit on 2026-08-13. Runtime fix and
repository verification were complete.
- **Summary:** With `ekp-auto-justify-mode` enabled, a newly typed trailing
space or tab can appear to do nothing. It becomes visible only after a
following non-whitespace glyph is typed. The same defect affects newly
typed leading whitespace and whitespace exposed by deletion.
- **Environment:** Text-property renderer after `task022`; Emacs 30.2.
- **Repro:**
1. Enable `ekp-auto-justify-mode` in a non-empty paragraph.
2. Move to the paragraph end and type one space.
3. Inspect the inserted source character or type one ordinary glyph.
- **Expected vs Actual:**
- Expected: every command-loop insertion is visible immediately on the
natural active line.
- Actual: the source space exists immediately, but EKP owns
`display ""` on it until another glyph makes it an interior character.
- **Investigation:** The live renderer correctly asks `ekp--project-line`
to leave the active line's glue natural, then separately applies static
paragraph-edge cleanup. Because the KP plan strips leading and trailing
whitespace from its content bounds, that cleanup hides active-line edge
whitespace after every edit.
- **Root Cause:** Static completed-paragraph edge policy leaks into the
unfinished active-line projection. The active line is only partly
natural.
- **Required Outcome:** Keep all source characters on the point-containing
active line natural, including leading/trailing spaces and tabs. Preserve
static edge cleanup on committed lines.
- **Fix:** `task023` prevents static paragraph-edge hiding from running on
the active line. When the first planned line is active, live cleanup also
starts at the source span beginning so an old hidden leading edge cannot
survive.
- **Verification:** Focused mark/edge regressions failed 0/3 before the
fix and passed 3/3 after it. The adjacent public-command matrix covers
consecutive whitespace/backspace, yank, newline, undo, active region,
overflow/pullback, paragraph exit, resize, IME, and teardown. In the
clean GUI run, one space advanced point 67→68 and cursor x 462→469 in
the same action; the source space had neither EKP-owned nor public
replacing display immediately or after redisplay. Typing and deleting a
following glyph left that state unchanged.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; runtime work previously implemented.
- **Commit:** — (documentation-only closure).
## Related Audit Scope
`task024` checks the same ownership boundary across character insertion,
consecutive whitespace, deletion/backspace, newline, yank, undo, point,
mark/region, visual-break boundaries, paragraph entry/exit, resize, and
mode teardown. New concrete defects receive new issue IDs rather than being
silently folded into these two symptoms.
## Verification Evidence
- Buffer ERT: 79/79.
- Full ERT: 170/170 in default order and 170/170 with seed `20260729`;
every test also passed in a fresh Emacs process.
- C/Elisp property fuzz: 300/300.
- Warning-as-error compilation: production files plus
`tests/ekp-buffer-tests.el`.
- checkdoc, pinned package-lint, release, dictionary, shell syntax,
artifact, stale-name, no-overlay, and diff checks pass.
- Dynamic artifact:
`/private/tmp/ekp-interaction-clean-rdZhdD`.
`report.md` returns `PASS`, `manifest.jsonl` records all assertions true,
and the temporal contact sheet contains one clean full-screen Emacs
window with no black segment.
- Artifact hashes:
- manifest:
`0eaeebd1697d69304cfd70189d7381a026b761333ab46ab1e2c5923a6e2db0c9`
- report:
`940efdb923a831dcd4b098cafee33b9a67c472f227be1897bd54cab0693f8b68`
- recording:
`519a4ba2757b453e54a2518c83784bfed26317eae4c3e246a984f38a2d30b2f5`
- first/final screenshots:
`8cd697130fd54b65bde6d1c06410f88c205779abce563b1e83661849dfc163e2`,
`11996136c3924aea9d539f77bd9db13b55310c089967c43091d2f61133a0d4db`

View File

@ -1,20 +0,0 @@
# Issue: Live Evaluator Source Freshness 2026-08-20
## issue028 [ ] Locked live evaluator can measure stale candidate bytecode
- **Status:** Closed by source-fresh evaluator verification on 2026-08-20;
performance gate remains open under the now-correct source measurement.
- **Summary:** `run-live-commit-evaluator.sh` loaded the candidate by name and
allowed an ignored local `.elc` to win. A report could therefore claim the
locked source-instrumented 16 ms gate passed while measuring bytecode.
- **Expected vs Actual:** Every baseline and candidate round must explicitly
`load-file` the four production source files for its code root. Before the
bootstrap fix, a narrowed matrix reported `pass:true`; afterward it reports
source candidate p99 above 16 ms and keeps `issue018` open.
- **Related:** `tests/run-live-commit-evaluator.sh`,
`tests/ekp-live-commit-evaluator.el`, `issue018`, `task030`.
- **Verification:** Source-load bootstrap, narrowed matrix, and source-path
live benchmark are complete; full locked matrix remains the task030 gate.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer evaluator verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,132 +0,0 @@
# Issue: Logical Text API Boundary 2026-07-28
## issue011 [x] Direct Elisp buffer APIs observe physical layout text
- **Status:** Closed by user re-audit on 2026-08-13. Runtime fix and
complete repository gate were implemented by `task019``task022`.
- **Summary:** The former buffer renderer wrote synthesized glue spaces,
soft newlines, discretionary hyphens, and hidden payload text into the
real buffer. Boundary adapters made save/copy/search appear logical, but
ordinary Elisp character APIs still observed the physical layout.
- **Environment:** Text-property renderer and synchronous live flow after
`task021`; Emacs 30.2.
- **Historical Repro:**
1. Insert text into a temporary buffer.
2. Run the pre-`task020` `ekp-justify-region` with a narrow width.
3. Compare the original text with `buffer-string` or
`buffer-substring-no-properties`.
4. Compare the same region through `filter-buffer-substring`.
- **Historical Evidence:** The pre-fix batch probe produced a 39-character
physical representation from a 30-character logical string.
`buffer-string` did not equal the original, while the boundary-filtered
substring did.
- **Expected vs Current:**
- **Expected:** Ordinary Elisp buffer-text APIs observe the original
character sequence while EKP's justified presentation remains visible
and editable.
- **Current implementation:** `buffer-string`,
`buffer-substring-no-properties`, direct character access, ordinary
search, syntax, markers, and saving operate on the unchanged source
characters. APIs that preserve or inspect text properties may still
observe EKP's display metadata; copy/kill strips that owned metadata.
- **Investigation:**
- The former `ekp-justify-region` deleted source text and inserted the
physical rendered string.
- Reversible `ekp-glue`, `ekp-soft-break`, `ekp-soft-hyphen`, and
`ekp-hidden` markers made restoration possible but did not keep direct
buffer reads logical.
- Save/copy/isearch adapters compensated at individual boundaries and
could not cover arbitrary Elisp consumers.
- The correct owner is the buffer representation: source characters must
remain authoritative, and layout must be presentational state only.
- **2026-07-29 Design Evidence:**
- The user ruled out overlays and selected text properties on existing
source characters as the only projection mechanism.
- `(min-width ((PIXELS)))` can add an exact absolute-pixel minimum width
without adding buffer characters.
- `(space-width FACTOR)` can shrink or stretch existing ASCII spaces.
Combined with `min-width`, it rendered a natural 7px space at exact 3px
and 20px targets in a clean GUI Emacs 30.2 probe while source text
remained `"a b"`.
- `line-prefix` can provide display-only indentation for
non-continuation lines. Native continuation lines still require
`wrap-prefix`.
- A replacing `display` string on an existing grapheme can show the
grapheme followed by a discretionary hyphen and visual newline. A GUI
probe displayed `d-\n` from the source `d` in `abcdefgh`; the source
stayed eight characters, two screen lines were produced, one
`vertical-motion` landed on source position 5, and a 20px
`line-prefix` applied to both rows.
- The proposed Latin hyphen owner is the complete grapheme immediately
before the selected break. Its display string reproduces that grapheme,
appends the hyphen and newline, and uses the `cursor` property to keep
point on the source glyph.
- Full reasoning, source links, limitations, and verification gates are
recorded in `tech-refer_text_property_layout_20260729.md`.
- **Root Cause:** Logical document state and rendered layout shared one
mutable character sequence. Reversible markers made the physical
representation recoverable, not logical.
- **Required Outcome:** Remove the leak at the representation owner rather
than adding per-command adapters. Ordinary Elisp character consumers must
see logical content without losing justified display, exact round-trip,
or seamless editing.
- **Implemented Design:**
- Logical source characters stay in the real buffer.
- The buffer renderer creates no overlay and inserts no layout character.
- Existing ASCII spaces use combined `space-width` and absolute-pixel
`min-width`; zero-source CJK/mixed glue uses `min-width` on the
preceding complete grapheme; indentation uses `line-prefix`.
- CJK breaks and Latin discretionary hyphens use replacing display
strings on an existing complete grapheme.
- Projection values have exact EKP owner properties, are nonsticky, and
update under `with-silent-modifications`.
- Foreign `display`, `line-prefix`, `wrap-prefix`, `composition`, and
`invisible` owners make only the affected hard paragraph natural and
diagnostic.
- One buffer uses the narrowest live window as its authoritative width;
simultaneous independent per-window plans are not claimed.
- Live editing uses a natural unfinished line, bounded overflow/pullback,
a stable active-flow anchor, line-signature convergence, IME deferral,
stale-generation rejection, and bounded automatic paragraph work.
- **Fix:** `ekp.el` now exposes a reusable semantic layout plan while
retaining the compatible returned-string API. `ekp-buffer.el` consumes
that plan as a source-clean text-property projection and runs a distinct
synchronous live-flow state machine.
- **Verification to Date:**
- Buffer ERT passes 79/79, including unchanged source/tick/undo/modified
state, direct logical reads, save/copy/isearch, foreign ownership,
no-overlay checks, exact property forms, overflow, deletion pullback,
exact type/delete break restoration, immediate active-line edge
whitespace, active/inactive mark preservation, anchor lifecycle, IME,
undo, resize, multiwindow width, and overload behavior.
- A clean Emacs 30.2 static probe passes exact 164px ASCII and
zero-source CJK glue, display-only breaks/hyphens, `line-prefix`, point,
vertical motion, region, mouse, unchanged source, and zero overlays.
The seven-case scale/remap/fringe/width matrix passes.
- A 31.71s dynamic recording passes before/immediate/settled overflow and
deletion checkpoints. Immediate and settled source/projection hashes
match; deletion restores the original source/projection hashes and
break count; every assertion is true and temporal review passes.
- Final-source default and seeded-permuted ERT pass 170/170; every one of
the 170 tests passes in a fresh Emacs process; C/Elisp fuzz passes
300/300.
- Warning-as-error Elisp compilation, checkdoc, pinned package-lint,
release/dictionary/static gates, all four C build profiles, full diff
review, and anti-slop cleanup pass.
- Independent final code review returns `APPROVE` with no findings;
independent architecture review returns `CLEAR`.
- **User Confirmation:** On 2026-07-28, the user explicitly defined the
target as clean text through Elisp APIs and requested recording only,
with implementation deferred. On 2026-07-29, the user resumed design,
excluded overlays, selected text properties, identified `min-width`,
`space-width`, and `line-prefix`, requested a non-polluting hyphen
implementation, then explicitly requested complete implementation under
the persistent goal model. Final visible-behavior acceptance was provided
by the user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; runtime work previously implemented.
- **Commit:** — (documentation-only closure).
- **Related:** `ekp.el`, `ekp-buffer.el`, `task017``task022`,
`spec_text_property_layout_engine_20260729.md`,
`tech-refer_text_property_layout_20260729.md`,
`postmortem/20260729-text-property-live-layout.md`.

View File

@ -1,97 +0,0 @@
# Issue: Narrow Live-Append Latency 2026-07-29
## issue018 [x] Unique live appends miss the frame budget at very narrow widths
- **Status:** Resolved. Native live-append backend and the locked source-fresh
performance gate are verified.
- **Summary:** Stable transactions removed per-key whole-hard-line planning,
and task030 incrementally extends paragraph preparation, DP state, and
layout lines only at structural row crossings. The user-selected
`ekp-auto-justify-native-append` backend now uses the loaded C 1D DP for
prepared live appends even when full layout is configured for Elisp.
Byte-compiled production paths and the source-fresh evaluator are within the
16 ms frame budget.
- **Environment:** Emacs 30.2 on macOS, C backend 1.6, benchmark width
fixed at 80 px, `gc-cons-threshold` bound to
`most-positive-fixnum` so mutator work is measured without GC pauses.
- **Repro:**
1. For the production-shaped check, byte-compile the four production
Elisp files into a temporary package root, put that root first on
`load-path`, and run `tests/ekp-buffer-live-bench.el` once with the C
module loaded and once with `ekp-use-c-module` nil.
2. Confirm `ekp--dp-run-1d` is byte code and inspect the `append` and
`hard-boundary` rows.
3. For the locked source/instrumentation matrix, run
`tests/run-live-commit-evaluator.sh` and inspect
`.omx/goals/performance/narrow-live-commit/latest-report.json`.
- **Expected vs Actual:**
- Expected: live append p99 remains below the 16 ms interaction budget,
including narrow windows.
- Actual production path: three repeated byte-compiled public-command runs
measure append p99 at 1.1581.326 ms for C and 1.4291.438 ms for pure
Elisp. Hard-boundary p99 is 1.2511.363 ms and 1.4571.470 ms.
- Actual locked evaluator (2026-08-20): four interleaved source-fresh rounds
across 64/80/96/128/160 px, 2/4/8/16 rows, both engines, and both GC
modes measure width-80 C at 12.010/14.622 ms p95/p99 and the
Elisp-configured live path at 10.980/11.194 ms. Paired improvements are
77.72/74.03% and 94.19/94.57%, respectively.
- **Investigation:**
- The task029 point-motion change is not the cause. Navigation is now a
zero-work path and does not enter planning or projection publication.
- The old workload produced 291 distinct hard-line strings and 291 plan
calls. `task031` removes that obsolete control flow: only 15 actual
visual-row crossings invoke `ekp-layout-plan`.
- The remaining high percentiles coincide with those allowed structural
commits and must be profiled as commit latency, not average key latency.
- Wider measurements are materially faster; the pathological result is
width-sensitive rather than a general point-motion regression.
- The previously recorded 6.399 ms task028 append p99 is not
reproducible with the current checked-in benchmark and must not remain
the current performance claim.
- Before the native backend, both backends missed p99 at structural
boundaries, so task030 evaluated both rather than optimizing only the C
wrapper.
- The frozen C wrapper measured only 2.615/2.655 ms p95/p99 and the
candidate 0.697/0.701 ms. Paragraph preparation, DP state reconstruction,
plan construction, and publication owned the end-to-end cost.
- Byte compilation removes most of the pure-Elisp interpreter and closure
overhead. The final source-fresh gate is now independently green; the
production result is retained as a separate deployment check.
- Rust is not selected. It would use the same `emacs_env` ABI and cannot
remove Elisp-owned font measurement, transaction, or projection work.
- **Required Outcome:**
- Diagnose and reduce unique-state narrow-width append planning cost
without changing KP output semantics, the core DP contract, C ABI, or
source-clean display model.
- Keep cache identity exact; do not introduce approximate/stale plan
reuse, timers, skipped edits, global GC changes, or point-based
invalidation.
- Preserve the task029 invariant that point-only motion performs zero
plan, cache, and text-property work.
- **Fix:** `task030` retains exact prepared paragraph/context data, extends
property-free append tails from the last complete-word boundary, resumes
pure-Elisp DP from a safe reachable state, reuses common layout lines, and
reconstructs only the live dirty source island. Unsupported contexts take
the unchanged full path. C int32 validation now performs one extraction.
- **Native backend update (2026-08-20):** `ekp-auto-justify-native-append`
now routes prepared 1D live append DP through the loaded C module even when
full layout uses Elisp. The option can be disabled for a pure-Elisp live
path, and unavailable modules fall back exactly. In a bounded source-fresh
all-width, 2/4/8/16-row, GC-excluded round first reduced append-DP and left
a noisy C p99 near the target. The complete four-round locked run then
measured width-80 total p95/p99 at 12.010/14.622 ms for C and
10.980/11.194 ms for the Elisp-configured live path, closing the gate.
- **Verification:** Exact baseline/C/Elisp hashes and append-chain
equivalence pass across 64/80/96/128/160 px, 2/4/8/16-row fixtures,
default/excluded GC, unsafe fallbacks, and randomized chains. Current
source-first normal and random-order ERT pass 296/296; the native bridge
parity regression passes 1/1; 300 fuzz cases, warning-clean production/C
compilation, C contract tests, release/dictionary checks, and reviewed
temporal GUI evidence pass. The complete four-round source-fresh evaluator
also passes every performance condition.
- **User Confirmation:** User selected architecture option 2 (`2`) and the
complete source-fresh gate now passes.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Codex.
- **Commit:** `3d3dda6` (native backend), `d216c6b` (phase record), and this
closure-record commit (complete four-round gate closure).

View File

@ -1,50 +0,0 @@
# Issue: Native Soft Wrap 2026-07-29
## issue015 [x] Narrow split windows truncate live text
- **Status:** Closed by user re-audit on 2026-08-13. Implementation and
independent verification were complete.
- **Summary:** With `ekp-auto-justify-mode` enabled in a narrow
side-by-side window, typing past the right edge horizontally scrolls the
line and displays a `$` truncation indicator instead of naturally
soft-wrapping.
- **Environment:** Graphical Emacs 30.2; side-by-side windows; native
progressive live layout.
- **Repro:**
1. Split a graphical frame side by side until the editing window is
narrower than 50 columns.
2. Enable `ekp-auto-justify-mode`.
3. Type a hard line beyond the right edge.
4. Observe horizontal scrolling and the left `$` indicator.
- **Expected vs Actual:**
- Expected: the active hard line follows native soft wrapping, point
moves onto a new visual row, and EKP may align only the row just left.
- Actual: no visual row is created, so native-row publication cannot
begin.
- **Root Cause:** Emacs 30.2 defaults
`truncate-partial-width-windows` to `50`. In narrower side-by-side
windows that setting overrides `truncate-lines=nil` and enables
truncation. The auto mode declared native soft wrap as an algorithmic
precondition but did not own the corresponding display variables.
- **Required Outcome:** While the mode is enabled, force native soft
wrapping in full-width and partial-width windows. On disable or
major-mode change, restore both the prior values and whether each value
was buffer-local.
- **Fix:** `task027` snapshots both truncation variables and their
buffer-local ownership before enabling native wrapping. Normal disable,
major-mode teardown, and failed activation restore that state exactly;
activation errors remain visible after cleanup instead of leaving a
half-enabled mode.
- **Verification:** The lifecycle regressions pass 3/3; buffer ERT passes
77/77; default, seed-`20260729`, and isolated ERT pass 166/166; C/Elisp
fuzz passes 300/300. Warning-as-error compilation, checkdoc, pinned
package-lint, release, dictionary, pinned-source, and static gates pass.
Retained dynamic evidence in
`/private/tmp/ekp-soft-wrap-final-pass-PIUigY` shows per-character typing
naturally advancing from one to two visual rows in a 44-column split,
with `hscroll=0`, exact source text, zero overlays, and zero live
replacing breaks.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; implementation complete.
- **Commit:** — (documentation-only closure).

View File

@ -1,54 +0,0 @@
# Issue: Natural Live Editing 2026-07-29
## issue014 [x] Near-edge editing publishes premature KP layout
- **Status:** Closed by user re-audit on 2026-08-13. Implementation and
independent verification were complete.
- **Summary:** With `ekp-auto-justify-mode` enabled, typing near the right
edge can publish several visual KP breaks and a discretionary hyphen
while the user is still composing the current hard paragraph. The result
does not feel like ordinary Emacs editing.
- **Environment:** Text-property live-flow renderer after `task024`;
graphical Emacs 30.2; mixed Latin/CJK prose.
- **Repro:**
1. Enable `ekp-auto-justify-mode` in a graphical window.
2. Type mixed Latin/CJK prose continuously toward the right edge.
3. Observe the transition one character at a time as Emacs naturally
wraps.
- **Expected vs Actual:**
- Expected: the source tail containing point is displayed exactly as
ordinary Emacs would display it. Before natural wrap, EKP publishes no
layout property there. After natural wrap, EKP may align only the row
the user has left. A complete KP pass runs only after the hard
paragraph ends or point leaves it.
- Actual: bounded KP flow selects and publishes display breaks during
typing. `natural-p` suppresses glue projection only; it still permits
prefix and break/hyphen projection.
- **Root Cause:** The live model treats an unfinished paragraph as a
continuously maintained partial KP plan. “Natural active line” is only
a glue exception inside that plan, not native Emacs display ownership.
- **Required Outcome:** Replace partial live KP with three explicit states:
a natural active tail, locally aligned rows already left by natural
wrapping, and a complete KP paragraph after completion. No live row may
add a discretionary hyphen or replacing visual break.
- **Fix:** `task025` deletes the partial-KP
anchor/lookahead/convergence/push-pull state machine. The active hard
paragraph is restored to native display before each source edit. After
the edit, EKP derives completed native visual rows from Emacs redisplay
and projects only their internal gap widths. The point-containing tail
stays native, and only hard-paragraph completion or paragraph exit runs
the complete KP planner. Live projection cannot install replacing
breaks, prefixes, or discretionary hyphens.
- **Verification:** Focused live contract ERT passes 4/4; buffer ERT passes
75/75; default, seeded-permuted, and isolated full ERT pass 164/164; fuzz
passes 300/300. Warning-as-error compilation, checkdoc, pinned
package-lint, release, dictionary, pinned-source, static ownership,
no-overlay, and diff gates pass. The retained clean Emacs 30.2 dynamic
artifact at `/private/tmp/ekp-native-live-2dYu2U` proves native
mixed-text wrap, exact deletion restoration, and the sole full-KP
transition after hard newline; every immediate and redisplay checkpoint
passes, with zero live owned breaks and no black recording segment.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; implementation complete.
- **Commit:** — (documentation-only closure).

View File

@ -1,69 +0,0 @@
# Issue: Final-Pass CJK Isolation 2026-08-01
## issue021 [x] Final-pass emergency layout can isolate CJK source lines at narrow widths
- **Status:** Resolved. Developer verification and independent review passed;
the user accepted the visible result and requested the commit.
- **Summary:** At narrow measures, the final K-P pass can choose one-glyph CJK
source lines in the mixed showcase paragraph. The visible symptom was first
found around an explicit hard atom, then reproduced after automatic inline
policy was split from hard atoms.
- **Environment:** Graphical Emacs 30.2 on macOS, narrow showcase widths,
justify alignment, C module active for 1D layout. The same core problem is
visible through the public buffer path, so this is not a renderer-only
issue.
- **Repro:** Run `M-x ekp-showcase`, set a narrow width such as 168px or
280px, and inspect the mixed Chinese/Latin inline paragraph. The failure
appears when a CJK glyph becomes a complete source line even though the
paragraph has legal non-emergency alternatives.
- **Expected vs Actual:** Ordinary underfull candidates in the final pass
should remain normal K-P candidates: adjustment ratio, badness, fitness,
and demerits decide the global path. Separately, the final pass must not
lose all reachability when a legal candidate is overfull. The previous
model confused that reachability safeguard with a hard/atomic fixed-cost
scoring shortcut, and an empty-plan oracle could hide the failure.
- **Investigation:** The strict pass is not the bug. The buffer projector
reproduces the semantic plan exactly and owns no line-choice compensation.
The root is in core DP final-pass modeling: ordinary underfull candidates
must receive finite emergency stretch and stay inside normal K-P scoring.
TeX's `artificial_demerits` instead preserves the last active path in the
final pass independent of content class; it is not owned by CJK, units,
hard atoms, or their adjacency.
- **Required Outcome:** Eliminate unintended isolated CJK source lines at the
checked narrow showcase widths without adding CJK-orphan, number-unit, or
screenshot-specific penalties. Preserve source text, zero overlays,
strict-pass behavior, Elisp/C parity, and the public C ABI.
- **Fix Plan:** Keep strict K-P unchanged. In the final pass, add a
TeX-style fixed emergency-stretch dimension to ordinary underfull
candidates and score them through normal badness, fitness, and demerits.
Expose the dimension as `ekp-emergency-stretch-pixel`: nil auto-resolves to
roughly three display-font `M` widths, while a non-negative integer fixes
the pixel value. If an overfull candidate would otherwise remove the final
active path to a breakpoint and no non-overfull candidate survives there,
install the best provisional path with tight fitness and zero incremental
demerits. The Elisp 1D path, looseness/parshape path, renderer semantics,
and C engine share this rule without changing the 15-field C paragraph ABI.
- **Verification:**
- Focused core single-CJK regressions pass 2/2.
- The implementation lane's focused G009 suite covering core, C parity,
hard atoms, public buffer projection, and GUI oracle passes 18/18.
- Warning-as-error Elisp byte compilation and the C build pass.
- Full regression falsified task035's line-width-sized emergency stretch:
the stretch must be a fixed dimension, not derived from each candidate
measure.
- Task036's fixed-dimension RED cases pass and the three regressions broken
by task035 remain green. Direct regression proves artificial lines add
zero demerits and preserve a complete nonempty source-covering plan.
- Focused root regressions pass 8/8, emergency selection passes 10/10,
core/buffer/GUI oracle passes 8/8, full ERT passes 288/288, seeded and
isolated suites pass 279/279, fuzz passes 300/300, and all build/static/
release/performance gates pass.
- Reviewed 42.78-second fullscreen single-window evidence at
`/tmp/ekp-g009-evidence-retry.UOpPNp` covers 480→168→280→168 and
no-hyphen→normal policy change, reports no isolated CJK source lines,
exact source, zero overlays/stale spans, active C, and `VERDICT=PASS`.
- **User Confirmation:** On 2026-08-02, the user replied “可以了,提交吧”,
accepting the visible result and requesting submission.
- **Resolved At:** 2026-08-02.
- **Resolved By:** `task036`; final-pass active-path preservation in core K-P.
- **Commit:** This commit (`fix: restore TeX final-pass reachability`).

View File

@ -1,81 +0,0 @@
# Issue: Point-Motion Projection Stability 2026-07-29
## issue017 [x] Point-only motion changes an already published live projection
- **Status:** Closed by user re-audit on 2026-08-13. The zero-work
point-motion invariant remains current; `issue019`/`task031` supersede
frontier-owned immediate publication and extend it across navigation.
- **Summary:** `task028` correctly made one whole hard line the planning
unit, but it also made transient point position the persistent live
projection boundary. Moving point across semantic plan lines therefore
removes and reinstalls display properties even though source text,
authoritative width, font metrics, and layout options are unchanged.
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; one hard
line with a multi-line semantic KP plan.
- **Repro:**
1. Edit the end of a long hard line so EKP publishes a semantic prefix.
2. Move point backward into an already projected semantic line without
changing source text.
3. Observe the prefix lose projection; move point forward and observe it
return.
- **Expected vs Actual:**
- Expected: point-only motion inside the active hard line is display
read-only. The existing plan, editing frontier, active semantic index,
line signatures, projection properties, source text, and cache state
remain unchanged.
- Actual: `post-command-hook` calls
`ekp-buffer--publish-live-boundary`, derives a new active index from
point, and mutates the projected prefix.
- **Root Cause:** The live state stores a plan-relative `active-index` but
no source-relative editing frontier. `ekp-buffer--project-live-plan`
therefore reads `(point)` both after source edits and during unrelated
reprojection. This gives cursor navigation ownership of layout state.
- **Required Outcome:**
- The latest real source edit owns a source-relative editing frontier.
- `before-change-functions` naturalizes the semantic line actually being
edited and its suffix before source mutation.
- `after-change-functions` moves the frontier to the changed region's
new end and publishes exactly once.
- Point-only motion inside the same hard line performs no plan, cache, or
text-property work.
- Width/font/layout-context changes may rebuild the plan, but they map
the preserved frontier into that plan rather than reading transient
point.
- Point-only motion, including leaving the hard line, performs zero work.
The next real edit elsewhere, hard newline, or explicit refill owns the
completed-paragraph transition.
- Core DP, C ABI, DP schema, source text, and the no-overlay contract do
not change.
- **Fix:** `task029` adds a source-relative frontier marker to live state,
derives the active semantic index from it, moves it only after real
changes, preserves it through reflow, and removes point-driven
publication from `post-command-hook`. The hard-line completion path is
unchanged.
- **Verification:**
- The focused point-motion test failed before implementation because
cursor movement removed the owned projection; the reflow regression
also selected point's line instead of the edit frontier. Both pass
after the fix.
- Independent focused ERT passes 10/10; buffer ERT passes 93/93.
Default, seed-`20260729`, and isolated full suites pass 182/182;
C/Elisp fuzz passes 300/300.
- Warning-as-error compilation, checkdoc, pinned package-lint, release,
dictionary, pinned-source, no-overlay, no-stale-symbol, and diff gates
pass.
- Point motion measures 0.033 ms p99 on the C backend and 0.037 ms in an
independent Elisp-backend review, with zero planner/cache calls.
- The reviewed GUI run
`/private/tmp/ekp-frontier-live-v3-66WYRW` records 39 manifest lines
over a 26.6-second real screen capture. Backward and forward motion
preserve projection hash, plan object, generation, cache size, active
index, and frontier. Delete, yank, real undo, narrow/restore resize,
and hard newline also pass; source is exact, overlays are zero,
`hscroll` is zero, no black segment is detected, and the evidence
report returns PASS.
- Independent architecture review is CLEAR. Independent code review
reports zero blockers; its only residual is the separate narrow-width
append latency tracked by `issue018`/`task030`.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; runtime work previously implemented.
- **Commit:** — (documentation-only closure).

View File

@ -1,273 +0,0 @@
# Issues: Repository System Audit 2026-07-28
Detailed evidence and prioritization live in
`Docs/REPOSITORY_AUDIT_20260728.md`.
## issue001 [x] Incomplete cache signatures reuse stale layout results
- **Status (2026-07-28):** Resolved by `task002` and `task003`.
- **Summary:** DP and automatic paragraph caches omitted configuration
values that determine the result.
- **Environment:** `main@29cef97`, Emacs 30.2, C module 1.5.
- **Repro:** Compute a paragraph, change `ekp-hyphen-penalty` or
`ekp-default-cws-stretch-pixel`, and compute again without clearing caches.
- **Expected vs Actual:** The second result must match a fresh computation;
it instead reuses the old cached object/result.
- **Investigation:** DP reuse and paragraph preprocessing have separate
owners, so each needs a complete structural signature.
- **Fix:** `task002` keys DP by width, looseness, and six runtime cost
parameters. `task003` shares one spacing signature between the paragraph
hash and same-string fast path, without adding watchers.
- **Verification:** Focused red/green cache tests; full ERT 99/99; C/Elisp
fuzz 300/300; warning-clean byte compilation; checkdoc clean.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P1-01, P2-01; `task002`, `task003`.
## issue002 [x] Failed saves leave a justified buffer unformatted
- **Status (2026-07-28):** Resolved by `task004`.
- **Summary:** The pre-save hook reverses layout, but a failed write does not
run an unconditional restoration path.
- **Environment:** `main@29cef97`, Emacs 30.2.
- **Repro:** Justify a visited buffer, make its target directory unavailable,
then call `save-buffer`.
- **Expected vs Actual:** The save should fail while preserving the visible
justified state; the buffer remains unformatted and retains stale save
markers.
- **Investigation:** Restoration existed only in `after-save-hook`; no hook
could provide finally semantics across every write failure.
- **Fix:** Whole-buffer writes switch to a logical copy from
`write-region-annotate-functions`; the display buffer is never
unformatted.
- **Verification:** Successful write, missing-directory failure, forced
encoding error plus retry, and `quit`; full ERT 102/102; fuzz 300/300;
warning-clean byte compilation; checkdoc clean.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P1-02; `task004`.
## issue003 [x] Buffer integration is not composable or lifecycle-owned
- **Status (2026-07-28):** Resolved by `task005`.
- **Summary:** EKP overwrites an existing copy filter and leaves hooks/filter
installed after manual unjustify removes the final layout span.
- **Environment:** `main@29cef97`, Emacs 30.2.
- **Repro:** Install a buffer-local `filter-buffer-substring-function`,
justify, copy, then unjustify the whole buffer.
- **Expected vs Actual:** Both filters should retain their semantics and
unused integrations should be removed; the prior filter is bypassed and
EKP integrations remain.
- **Investigation:** Installation used direct `setq-local`; public and
internal unjustify shared one body with no lifecycle boundary.
- **Fix:** Record local/inherited filter ownership, run the prior filter
before EKP logical inversion, split internal structural unjustify from the
public cleanup wrapper, and observe external deletion with a guarded
after-change hook.
- **Verification:** Public copy and DELETE filter paths; local/global
restoration; final unjustify; mode disable; full ERT 107/107; fuzz
300/300; warning-clean byte compilation; checkdoc clean.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P1-03, P2-06; `task005`.
## issue004 [x] Test order dependency produces a false green suite
- **Status (2026-07-28):** Resolved by `task006`.
- **Summary:** The named parshape C-bypass test exercises first-line indent
and passes only after another test leaks `ekp-use-c-module=nil`.
- **Environment:** `main@29cef97`, Emacs 30.2, C module 1.5.
- **Repro:** Load the C module and run only
`ekp-test-parshape-bypasses-c`.
- **Expected vs Actual:** The test should independently prove the real
parshape dispatch path; it fails in isolation while the full suite passes.
- **Investigation:** The test binds the wrong variable and fixtures restore
only a subset of mutable global state.
- **Fix:** Bind the real parshape and drive `ekp-pixel-justify`; dynamically
restore every isolated EKP tunable; provide permuted-order and
fresh-process runners, with the former in CI.
- **Verification:** Focused 0/1 red then 3/3 green; reproducibly permuted
ERT 108/108; all 108 ERT tests passed independently in fresh Emacs
processes.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P1-04; `task006`.
## issue005 [x] C API and build boundary need explicit validation
- **Status (2026-07-28):** Resolved by `task008` and `task009`.
- **Summary:** Malformed direct calls can signal despite the former nil-only
contract, extreme int32 arithmetic is not closed, and the interactive
build command uses an unquoted shell path.
- **Environment:** `main@29cef97`; macOS arm64; C11 toolchain.
- **Repro:** Pass a malformed field vector directly; inspect int32
accumulation and `ekp-c-module-build` path construction.
- **Expected vs Actual:** Inputs, error semantics, ranges, and build profiles
should be explicit; the current boundary mixes signaling, clamping, and
shell assumptions.
- **Investigation:** Fifteen-field schema checks happen piecemeal; arithmetic
continues in int32; default flags include native/LTO optimization.
- **Fix:** C 1.6 preflights the complete schema and uses
`ekp-c-invalid-input`; DP intermediates are int64 and backend signals
surface. Builds use direct argv and explicit portable/native/debug/
sanitize profiles, with portable default.
- **Verification:** Direct boundary 0/6 → 6/6; dispatcher 0/1 → 1/1;
build process/profile 0/2 → 2/2; full ERT 116/116; fuzz 300/300; four
warning-clean profiles; real interactive and whitespace-path portable
builds. ASan runtime loading is blocked by macOS policy and remains a
Linux CI gate.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P2-02, P2-03; `task008`, `task009`.
## issue006 [x] Dictionary claims exceed parser and provenance coverage
- **Status (2026-07-28):** Resolved by `task011`.
- **Summary:** Alternative pattern syntax is skipped, several license/source
companions are absent, and the update script is unpinned.
- **Environment:** 50 bundled `hyph_*.dic` files at `main@29cef97`.
- **Repro:** Count non-comment patterns containing `/` and compare dictionary
files with same-name README files.
- **Expected vs Actual:** Supported syntax and provenance should be auditable;
2399 alternative patterns across three languages are ignored and 20
dictionaries lack same-name README files.
- **Investigation:** The parser intentionally skipped `/` patterns; deeper
review found 2399 libhyphen replacement rules across Hungarian, Catalan,
and Albanian plus four Esperanto slash-prefixed rules. The updater pulled
moving HEAD without a manifest/checksums, and Sanskrit had no exact license
evidence at the pinned snapshot.
- **Fix:** Reject those four dictionaries with a typed condition instead of
corrupt partial support; pin 49 files and their checksum/source/license
evidence; remove unverifiable Sanskrit; add portable check/export tooling.
- **Verification:** Golden red 0/2 → 2/2; offline/upstream 49/49; two
identical exports; full ERT 121/121; compile/checkdoc/shell/YAML/diff pass.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P2-04; `task011`.
## issue007 [x] GUI verification reports failure without failing automation
- **Status (2026-07-28):** Resolved by `task007`.
- **Summary:** The full fit command lives only under `tests/`, while its
noninteractive matrix prints failures but exits successfully.
- **Environment:** `main@29cef97`, GUI Emacs 30.2.
- **Repro:** Load `tests/ekp-gui-verify.el`; force a failing row in
noninteractive matrix mode.
- **Expected vs Actual:** Installation/loading instructions should be clear
and any failed row should return nonzero; failure is currently textual
only.
- **Investigation:** The diagnostic and matrix have no shared packaged
entrypoint or assertion boundary.
- **Fix:** Cases return structured results to one report boundary; batch
failure exits 1 after printing the table. Documentation explicitly loads
the developer tool and states it is not part of `ekp-region`.
- **Verification:** Forced-failure/success controls 2/2; default and
permuted full ERT 110/110; real GUI matrix 7/7; clean fullscreen
single-window screenshot inspected at
`/tmp/emacs-kp-gui-20260728-clean.png`.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P2-05; `task007`.
## issue008 [x] Hot-loop allocation and nil caching need benchmark gates
- **Status (2026-07-28):** Resolved by `task012`.
- **Summary:** Tokenization and hyphen insertion repeatedly rebuild strings,
and nil hyphen results are recomputed.
- **Environment:** `main@29cef97`, Emacs 30.2.
- **Repro:** Profile long tokens and query an unhyphenated word twice while
counting `ekp-hyphen--compute`.
- **Expected vs Actual:** Repeated nil results should hit the cache and
hot-loop growth should remain linear; nil results compute twice.
- **Investigation:** Cache lookup uses `or`, which cannot distinguish an
absent key from a cached nil.
- **Fix:** The word-position cache distinguishes absent entries with a
sentinel; tokenizer boxes accumulate reversed fragments and join once;
inserted words concatenate original slices once.
- **Verification:** Nil call-count red 0/1 → 1/1; exact long/propertized and
dense output regressions 3/3; 1k8k benchmark improved the 8k cases from
3.133/0.945 s to 1.100/0.013 s; full ERT 124/124; fuzz 300/300;
warning-as-error production compilation and checkdoc passed.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P3-01; `task012`.
## issue009 [x] Release and documentation governance are not closed
- **Status (2026-07-28):** Resolved locally by `task010`.
- **Summary:** Local version/tag state is ahead of the published remote,
CI dependencies float, Windows is absent, and ignored `.phrase` records are
called the source of truth.
- **Environment:** local `main@29cef97`, `origin/main@11437cb`.
- **Repro:** Compare local/remote commits and tags, CI pins, `.gitignore`, and
the repository workflow contract.
- **Expected vs Actual:** Release metadata and versioned decisions should be
mutually consistent; local `v1.0.0` is not present remotely and the process
source of truth is ignored.
- **Investigation:** No single release gate currently checks commit, tag,
changelog, package/C versions, CI, and artifacts together.
- **Fix:** `.phrase` is tracked truth; actions and package-lint are immutable
inputs; Windows compiles/runs the pure-Elisp baseline; an executable local
invariant gate and separate remote/artifact checklist own the release path.
- **Verification:** Gate red → green; pinned package-lint pass; workflow YAML
and shell syntax pass; default ERT 119/119; diff check pass.
- **User Confirmation:** User requested completion of all audit findings on
2026-07-28; the automated local acceptance criteria are satisfied.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P2-07; `task010`.
## issue010 [x] Same-string property mutation bypasses paragraph identity
- **Status (2026-07-28):** Resolved by `task016`.
- **Summary:** `ekp--para-key` includes layout-relevant text properties, but
the most-recent paragraph fast path compares only string identity plus a
partial configuration tuple.
- **Environment:** current remediation working tree; Emacs 30.2.
- **Repro:** Resolve a string, add `ekp-no-break` to the same string object,
then resolve it again without clearing caches.
- **Expected vs Actual:** The second lookup must equal a fresh property-aware
paragraph; it returns the pre-mutation paragraph object.
- **Investigation:** The fast path duplicates only part of the hash-cache
identity contract, so in-place property changes cannot invalidate it.
- **Fix:** Store `(string, complete ekp--para-key, paragraph)` in the
most-recent slot and compare the same structural key used by the hash
cache. Remove the six style-variable watchers that compensated for the
former partial identity.
- **Verification:** Same-object mutation red 0/1 → 1/1; focused cache matrix
6/6; default and seeded-permuted ERT 130/130; every one of 130 ERT tests
passed in a fresh Emacs process; fuzz 300/300; warning-clean Elisp/C
compilation, checkdoc, package-lint, release/dictionary/static gates, and
GUI matrix 7/7 passed. Independent architecture review returned `CLEAR`
for the shared-key ownership.
- **User Confirmation:** User requested completion of every repository
finding on 2026-07-28.
- **Resolved At:** 2026-07-28.
- **Resolved By:** Codex.
- **Commit:** Pending.
- **Related:** audit P1-05; `task016`.

View File

@ -1,97 +0,0 @@
# Issue: Semantic Live Prefix 2026-07-29
## issue016 [x] Live editing freezes native rows instead of projecting a semantic hard-line prefix
- **Status:** Closed by user re-audit on 2026-08-13. The whole-hard-line
commit result is retained; per-edit replanning was superseded by
`issue019`/`task031`.
- **Summary:** Before `task028`, live editing committed completed native
visual rows independently. User testing showed that earlier soft-wrapped
rows in the same hard line did not become globally KP-aligned while the
user continued typing. The behavior was row-local, not a dynamic
projection of one semantic hard-line plan.
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; mixed
Latin/CJK hard lines that wrap across multiple visual rows.
- **Repro:**
1. Enable `ekp-auto-justify-mode`.
2. Type one long hard line that naturally soft-wraps into multiple rows.
3. Continue editing on the current unfinished row without inserting a
hard newline.
4. Observe the rows before point.
- **Expected vs Actual:**
- Expected: EKP sends the complete current hard-line source text to the
existing `ekp-layout-plan` and projects only the complete semantic
lines before the line containing the latest real source edit. Those
preceding lines may change breaks and glue together whenever text,
width, font, or layout options change. The editing-frontier line and
all following source stay naturally editable. Point-only navigation
does not change the projection.
- Actual before `task028`: the live model derived native visual rows and
committed them one at a time. Once a row was treated as completed,
later edits could not let the whole hard-line KP plan revise earlier
breaks and glue as a single semantic decision.
- **Root Cause:** `ekp-buffer` owned live editing but used native visual-row
boundaries as the persistent layout unit. That was the wrong owner
boundary. Core DP already computed the correct complete-text plan; the
buffer layer needed to consume that plan and decide which prefix was safe
to project, not invent a row-freezing live algorithm.
- **Required Outcome:** Replace native-row commitment with semantic prefix
projection:
- keep the core DP, C ABI, DP schema, and `ekp-layout-plan` contract
unchanged;
- pass the full current hard line to `ekp-layout-plan`;
- find the plan line containing the latest real source edit;
- project only the complete plan lines before that line;
- leave the edit-frontier plan line and all following source natural;
- compare old/new line signatures so unchanged prefixes are not
reinstalled;
- cache recent full hard-line plans in `ekp-buffer` only;
- fail closed to native display for conflicts, oversized hard lines, or
stale generations;
- never use overlays or insert source layout characters.
- **Fix:** `task028` replaces the native-row commit state with one current
whole-hard-line semantic plan. `task029` makes the latest real source
edit, rather than transient point, own the natural-suffix frontier.
`ekp-buffer` projects only prior lines, owns break whitespace on the
preceding semantic line, keeps the editable suffix property-free,
reuses a 16-entry buffer-local plan LRU, and updates changed projection
suffixes from common-prefix line signatures. The core DP, C ABI, schema,
and `ekp-layout-plan` contract are unchanged.
- **Refinement:** `issue017`/`task029` supersede the point-driven boundary
portion of `task028`. The persistent natural-suffix boundary belongs to
the latest source edit; transient point motion is display read-only.
- **Later Refinement:** `issue019`/`task031` supersede immediate
whole-hard-line replanning after every edit. The plan remains authoritative
at structural commits; stable same-row editing is owned by a saved
committed projection and local dirty transaction.
- **Verification:**
- Public-path and edge-case ERT pass in the 181-test default suite, the
seed-`20260729` suite, and 181 isolated Emacs processes. C/Elisp fuzz
passes 300/300.
- Warning-as-error compilation, checkdoc, the CI-pinned package-lint,
release, dictionary, pinned-source, no-overlay, and source-clean gates
pass.
- The main dynamic GUI run
`/private/tmp/ekp-semantic-live-v4-vFZTkr` exercises typing, deletion,
backward/forward point motion, yank, real undo, narrow/restore resize,
and hard newline. Its 38 checkpoints have exact source text, zero
overlays, `hscroll=0`, no pending transaction, no failed assertion,
and a reviewed temporal verdict of PASS.
- The 44-column split-window run
`/private/tmp/ekp-semantic-split-v3-uPwuOi` proves native incremental
soft wrapping, one semantic projected prefix line, a natural active
line, exact source text, zero overlays, `hscroll=0`, clean disable, and
a reviewed temporal verdict of PASS.
- A subsequent task029 audit could not reproduce the original 6.399 ms
append p99 with the current checked-in benchmark. At its synthetic
80-pixel width, repeated GC-excluded runs instead record one plan per
unique append and roughly 3385 ms p99. `issue018`/`task030` track this
separate performance debt. Point motion remains a zero-work path at
about 0.03 ms p99.
- Final independent code review returns APPROVE; independent architecture
review returns CLEAR. Both reviewed the latest font-context
invalidation delta.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; runtime work previously implemented.
- **Commit:** — (documentation-only closure).

View File

@ -1,25 +0,0 @@
# Issue: Showcase Contract and Test Freshness 2026-08-20
## issue026 [ ] Showcase loses automatic inline policy and runners can false-green
- **Status:** Runtime/test fix verified by `task041`; user-visible
confirmation remains pending.
- **Summary:** The showcase's automatic inline-code face was commented out,
so clean source runs fail the GUI policy contract. Local stale ignored
`tests/*.elc` files can mask the regression, and random/isolated selectors
omit nine GUI verifier tests.
- **Expected vs Actual:** Clean, source-fresh normal/random/isolated runners
should execute the complete ERT inventory and retain the automatic inline
example. Current source has 3 failing showcase tests; stale local bytecode
reports a false green.
- **Related:** `tests/ekp-showcase.el`, `tests/run-tests.sh`,
`tests/run-tests-random-order.el`, `tests/run-tests-isolated.sh`,
`task041`.
- **Fix:** The automatic inline face is restored; one source loader uses
`load-file` for every production/test source and all runners select the
complete ERT registry.
- **Verification:** Source-first showcase 3/3, normal/random/isolated 295-test
inventories, and GUI contract checks pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** `6a8c7e0`.

View File

@ -1,72 +0,0 @@
# Issue: Stable Live Edit Transaction 2026-07-29
## issue019 [x] Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly
- **Status:** Closed by user re-audit on 2026-08-13. Implementation was
developer-verified by `task031`.
- **Summary:** The current live path makes one `frontier` simultaneously own
the latest edit, the natural suffix, and projection reuse. Before every
edit it clears the touched projected line and every following line; after
every edit it replans the complete hard line. This makes unrelated rows
move during continuous input and turns narrow append latency into a
symptom of the same ownership error.
- **Environment:** Graphical Emacs 30.2; `ekp-auto-justify-mode`; long mixed
Latin/CJK hard lines with an installed live prefix.
- **Repro:**
1. Type a hard line until multiple visual rows exist.
2. Continue typing within the unfinished row, or edit a previously
projected middle row.
3. Observe earlier/succeeding rows and the owned text properties after each
source change.
4. Delete a projected space and insert the identical space again.
- **Expected vs Actual:**
- Expected: ordinary same-row input leaves the committed projection
untouched. A middle-row edit opens one local dirty island while
unaffected break anchors remain installed. Small changes are absorbed by
existing glue/native wrapping; local words move only when the dirty row
no longer fits. Restoring the exact source restores the exact projection
immediately.
- Actual: `before-change` invalidates the touched line and its complete
projected suffix. `after-change` moves `frontier`, runs or reuses a
whole-hard-line plan, and republishes immediately. The transaction stores
only an old paragraph marker and deleted fragment, so it cannot restore a
committed baseline as a first-class state transition.
- **Root Cause:** `frontier` conflates three independent owners: source-edit
location, unprojected edit boundary, and plan/projection reuse boundary.
Signature diffing reduces writes after a plan is chosen, but cannot prevent
global optimal breaks from changing when a new plan is computed after
every key.
- **Required Outcome:**
- committed projection owns the last published source/plan/signatures/spans;
- a dirty edit transaction snapshots that baseline and owns only the
affected local island;
- ordinary same-row edits do no whole-hard-line DP and do not rewrite
unaffected projection;
- crossing a native visual-row boundary atomically replans the completed
hard-line prefix, leaving the new current row natural;
- the next real edit elsewhere, hard newline/paragraph completion,
explicit refill, or width/font/layout-context change is a commit event;
- point-only motion performs zero planning, cache, property, or layout work;
- a reversible edit restores source and the complete owned projection
`equal-including-properties` without depending on a best-effort replan;
- completed paragraphs still use the existing globally optimal core DP;
- no overlays or source layout characters are introduced.
- **Fix:** `task031` deletes frontier-owned immediate publication. Committed
live state now owns source/key/plan/signatures/prefix/spans; one edit
transaction snapshots that state and marker offsets, naturalizes only its
dirty island, restores exact baselines directly, and permits replanning
only at native-row or structural commit events. Static lazy-reflow chunks
exclude the active paragraph. The core DP and `ekp.el` are unchanged.
- **Verification:** Focused public-path regressions were observed red before
implementation and green afterward. Final buffer/default/random/isolated
ERT runs pass 99/99, 188/188, 188/188, and 188/188; C/Elisp fuzz passes
300/300; compile, checkdoc, package-lint, release, and local dictionary
gates pass. The reviewed dynamic run
`/private/tmp/ekp-stable-transaction-final5-2BFryc` is PASS with 55
checkpoints, no failed assertion, a completed run-end, and no black
segment or visual noise. Same-row and point-motion benchmark scenarios
perform zero plans; 291 appends now contain only 15 structural plans.
- **User Confirmation:** Provided by user re-audit on 2026-08-13.
- **Resolved At:** 2026-08-13.
- **Resolved By:** User re-audit closure; `task031` developer verification.
- **Commit:** — (documentation-only closure).

View File

@ -1,28 +0,0 @@
# Plan: Complete Automatic Spacing Cache Identity
## Scope
Finish `issue001` by fixing the auto-CWS paragraph-cache defect only.
Preserve the completed DP signature work in `task002`.
## Resolution Path
1. Prove the paragraph hash and same-string fast path both reuse stale
automatic spacing after `ekp-default-cws-stretch-pixel` changes.
2. Give paragraph preprocessing one spacing-signature owner shared by both
lookup paths.
3. Keep automatic font-derived values represented by the existing
font/display-context identity; add the remaining CJK default explicitly.
4. Prove an unchanged signature still hits instead of disabling caching.
5. Run focused and full ERT, fuzz, warning-clean compilation, and checkdoc.
## Non-goals
- No variable watcher for the CJK default.
- No DP-key or C ABI change.
- No editor-integration changes.
## Rollback
Revert the spacing-signature helper, the two lookup uses, and the three
regression tests. Caches are process-local, so no data migration is needed.

View File

@ -1,87 +0,0 @@
# Plan: Buffer Module Naming 2026-07-28
## Goal
Rename the editor-facing buffer integration module from `ekp-region` to
`ekp-buffer` so its file, feature, customization group, configuration
variables, private namespace, tests, and documentation match the
responsibility it actually owns.
## Intended User Path
- Load buffer integration with `(require 'ekp-buffer)`.
- Keep public operation names whose suffix describes their real operand:
`ekp-justify-region`, `ekp-unjustify-region`, `ekp-justify-buffer`,
`ekp-unjustify-buffer`, and the region protection commands.
- Keep `ekp-auto-justify-mode`, `ekp-org-setup`, `ekp-markdown-setup`,
`ekp-refill-paragraph`, and `ekp-diagnose`; they already follow the
package-level `ekp-` namespace and describe behavior rather than the old
module filename.
- Rename the module-owned public configuration surface and all private
implementation/test symbols from `ekp-region*` to `ekp-buffer*`.
## Scope and Order
1. Rename `ekp-region.el` to `ekp-buffer.el` and
`tests/ekp-region-tests.el` to `tests/ekp-buffer-tests.el`.
2. Rename the provided feature, customization group, module-owned settings,
internal functions/state, and test namespace.
3. Update test runners, GUI/showcase helpers, CI, contributor commands, and
release checks that name the files or symbols.
4. Update current public/developer/audit documentation and add an
Unreleased breaking migration note.
5. Preserve historical phase/postmortem wording; add a new decision record
instead of rewriting history.
## Compatibility Decision
This is an explicit breaking rename requested for the current Unreleased
line. Do not retain `ekp-region.el`, provide `ekp-region`, define obsolete
aliases, or add variable aliases. The migration is one direct replacement:
`ekp-region` becomes `ekp-buffer`. Public region commands retain their
semantic names.
## Behavior Lock
- Existing full ERT suite: 130 tests before the rename.
- Existing buffer tests cover exact round-trip, save failure, isearch,
kill/yank, undo, mode lifecycle, resize/lazy reflow, protection commands,
and whole-buffer/DWIM paths.
- The rename changes lookup names only; rendered output and persisted bytes
must remain identical.
## Cleanup and Fallback Inventory
- Smell: the `ekp-region` namespace names only one operation scope while
the module owns the entire buffer/editor lifecycle.
- No dead-code, duplication, error-handling, dependency, or UI pass is in
scope.
- The selected-window fallback in width calculation is a grounded
undisplayed-buffer boundary and remains unchanged.
- No masking fallback, swallowed error, broad compatibility shim, or
escalation candidate was found.
## Validation
- Focused load and buffer ERT through `ekp-buffer`.
- Default, seeded-permuted, and fresh-process-isolated full ERT.
- C/Elisp fuzz to prove rendered behavior remains unchanged.
- Warning-as-error byte compilation, checkdoc, package-lint/release gates,
shell syntax, and `git diff --check`.
- Scan active runtime, tests, CI, and current documentation for stale
`ekp-region` / `ekp-region-tests` references; historical records are the
only allowed occurrences.
- Read the complete diff before closing the task.
## Stop Condition
The new feature loads from `ekp-buffer.el`, all renamed tests and automation
pass, current documentation presents only the new API, historical references
are explicitly contextual, and no behavior or unrelated source changed.
## Result
Completed as `task017`. The approved direct rename is mechanically
equivalent to the prior runtime/test implementation apart from the
customization parent and explicitly private protrusion helper. All validation
listed above passed; no compatibility shim or unrelated cleanup was added.

View File

@ -1,31 +0,0 @@
# Plan: C API and Arithmetic Contract
## Scope
Resolve the P2-02 half of `issue005`: validate the complete public schema
before extraction, make caller errors explicit, and eliminate signed
overflow for valid inputs.
## Resolution Path
1. Lock malformed single/batch vectors, bad penalty scalars, out-of-range
integers, and a valid extreme-width calculation with direct C red tests.
2. Validate vector types, n/n+1 lengths, scalar types, positivity, and
signed 32-bit range before allocating or extracting.
3. Register one `ekp-c-invalid-input` condition for caller errors; retain
nil only for allocation/no-result.
4. Widen DP line-metric, adjustment, flexibility, and rest intermediates to
64-bit without changing the 15-field ABI.
5. Propagate module signals through the enabled Elisp dispatcher.
6. Build release/sanitizer profiles and run focused/full ERT plus fuzz.
## Non-goals
- No new algorithm or C tokenization.
- No change to valid 15-field layout or result shape.
- Build command/profile portability is the next atomic task.
## Rollback
Restore permissive extraction, int32 intermediates, C 1.5 requirement, and
the error-swallowing dispatcher. Valid persisted data is unaffected.

View File

@ -1,29 +0,0 @@
# Plan: Shell-Free Profiled C Builds
## Scope
Finish `issue005` P2-03: invoke make as argv in the module directory and
separate portable, native, debug, and sanitizer build intent.
## Resolution Path
1. Lock the interactive command shape with a whitespace-path process test
and an invalid-profile control.
2. Remove the one-use process wrapper and shell interpolation; use
`make-process` with a real argv and bound `default-directory`.
3. Make `portable` the Makefile default; isolate `native`, `debug`, and
`sanitize` flags behind explicit `PROFILE`.
4. Update CI and all build documentation to the same vocabulary.
5. Build every profile warning-clean, load/test the portable result, and
compile a copied source tree whose path contains spaces.
## Non-goals
- No dependency on CMake or another build system.
- No automatic compiler/package installation.
- No publishing or replacing a released artifact.
## Rollback
Restore the shell command and monolithic flags, then remove profile tests and
documentation. No persisted data changes.

View File

@ -1,42 +0,0 @@
# Plan: Complete DP Cache Signature
## Scope
Fix `issue001` P1-01 only: DP results must not alias when any
algorithm/cost parameter changes. The auto-CWS paragraph-key defect remains
open for a separate atomic task.
## Resolution Path
1. Drive the public `ekp-dp-cache` path with cached-then-changed parameters.
2. Prove each scenario differs from its baseline and matches a fresh
computation.
3. Replace the width/looseness special-case key with one complete flat
signature.
4. Use structural equality for DP-cache keys and prove identical signatures
still hit the cache.
5. Run focused tests, the complete suite, C/Elisp fuzz, byte compilation,
checkdoc, and diff/static checks.
## Data Ownership
The `ekp-para` DP cache owns reuse. Its key must therefore contain every
runtime value read by the DP that is not already frozen into that paragraph:
- line width and looseness;
- line, hyphen, adjacent-fitness, and consecutive-hyphen penalties;
- last-line minimum ratio and short-line penalty.
Paragraph-construction inputs remain owned by `ekp--para-key`.
## Non-goals
- No cache watchers or eager cache clearing.
- No C ABI change.
- No save/copy integration changes.
- No fix for the separate auto-CWS paragraph cache.
## Rollback
Revert the key/table change and the associated regression tests. No stored
file format or public API changes.

View File

@ -1,42 +0,0 @@
# Plan: Core Rule Ownership 2026-07-28
## Goal
Close audit P3-02 with the smallest net-value refactor: centralize only rules
that are currently duplicated across the hot DP/render paths, without
splitting files or wrapping workflows.
## Existing Owners
- DP reuse identity is already owned by `ekp--dp-key` and paragraph
construction identity by `ekp--para-key`/`ekp--spacing-signature`.
- Ideal line width is already owned by `ekp--line-ideal-pixel` and covered
by a brute-force test.
## Missing Owners
1. Edge-space exclusion is repeated in 1D DP, loose DP, line-glue rendering,
and ideal-width reconstruction.
2. The five layout-marker property names are repeated between the renderer
and region inversion detection.
## Resolution Path
1. Add red direct-rule tests against independent edge-space recomputation
and the complete marker vocabulary/nonstickiness contract.
2. Add one inline edge-space rule and reuse it at all four call sites.
3. Add one marker property constant and reuse it in the renderer and region
detection.
4. Retain existing exact round-trip, DP/C parity, fuzz, compile, and checkdoc
gates.
## Non-goals
- Do not split `ekp.el` or shrink the `ekp-para` struct cosmetically.
- Do not allocate a line-metrics object in the DP inner loop.
- Do not add accessors, adapters, or change the C ABI.
## Rollback
Inline the two rules again. Keep the direct invariants if a measurable
source-mode regression makes the inline helper unacceptable.

View File

@ -1,44 +0,0 @@
# Plan: Dictionary Syntax and Provenance 2026-07-28
## Goal
Make the 50 bundled dictionaries reproducible and accurately described:
every byte has a pinned source/checksum/license trail, and the parser's
handling of replacement/alternative patterns is explicit and tested.
## Scope
1. Confirm upstream replacement syntax and golden words for Hungarian,
Catalan, and Albanian before changing the parser.
2. Add failing tests for the chosen affected-language contract and updater
manifest validation.
3. Pin the LibreOffice dictionaries commit and replace GNU-specific,
moving-HEAD update commands with a fail-closed POSIX workflow.
4. Generate a deterministic manifest containing source path, SHA-256,
syntax flags, and local/upstream license evidence for every dictionary.
5. Document the supported syntax honestly. Implement replacement semantics
only if they fit the fixed-width box/DP model without corrupting logical
text or creating a second typography engine.
6. Run golden tests, manifest verification, a double-run reproducibility
comparison, full ERT, byte compilation, and checkdoc.
## Non-goals
- Do not add automatic language detection.
- Do not silently treat replacement patterns as ordinary Liang patterns.
- Do not move font measurement, tokenization, or rendering ownership into
the dictionary parser.
- Do not add a parser dependency.
## Decision Gate
Replacement patterns conditionally rewrite glyphs at a selected break. If
correct support requires break-specific box widths across Elisp DP, C
marshalling, and rendering, preserve the safe skip behavior, expose/count it,
add language golden limitations, and document that subset. Do not emit
linguistically incorrect breaks merely to increase pattern coverage.
## Rollback
Restore the previous parser and bundle together. A manifest must never claim
checksums or source paths for bytes that are not present.

View File

@ -1,87 +0,0 @@
# Plan: Final Cleanup and Review Gate 2026-07-28
## Goal
Remove residual low-signal code from the repository-remediation diff, prove
every documented acceptance criterion, and obtain independent code and
architecture approval.
## Behavior Lock
- Full ERT: 129/129.
- Seeded permuted ERT: 129/129.
- Every pre-owner ERT passed in a fresh Emacs process.
- C/Elisp fuzz: 300/300.
- Exact round-trip, public protection, C parity, save failure, and release
boundary regressions are present.
## Cleanup Scope
All changed runtime/test/build files, with edits limited to a proven smell.
Documentation is reviewed for drift, not rewritten cosmetically.
## Smells and Order
1. **Fallback gate:** classify every fallback-like path before editing.
2. **Dead code:** remove the unused paragraph accessor stack and route its
few test/render call sites directly to the owning `ekp-para`.
3. **Boundary scan:** replace direct use of dependency-private dispatch
internals with the public dispatcher while preserving prior-filter and
DELETE lifecycle semantics.
4. **Duplication/naming:** retain direct code where extraction would add
glue; make no speculative refactor.
5. **Tests:** add nothing unless cleanup exposes an unprotected behavior.
## Fallback Inventory
- Short-language dictionary resolution: grounded compatibility behavior;
deterministic and covered by `ekp-test-hyphen-lang-fallback`.
- Batch/tty font fallback: grounded external-display boundary; batch tests
and GUI matrix cover fallback and primary environments.
- Interactive C build/load catch: grounded outer command boundary; retains
build buffer and reports the error.
- Missing optional dictionary: grounded optional-resource fallback; only
not-found is caught, unsupported syntax propagates.
- C nil/allocation/no-result and thread-pool sequential paths: grounded
fail-safe/performance-boundary fallbacks; semantic errors propagate and
C/Elisp parity/fuzz cover equivalence.
- Test/fuzz catches: assertion harness boundaries, not production defaults.
No masking fallback, broad compatibility shim, swallowed business-logic
error, or escalation candidate was found.
## Cleanup Result
- Removed nine unused one-line paragraph accessors and routed the remaining
callers to the owning `ekp-para`.
- Removed the direct call to Emacs's private `buffer-substring--filter`.
EKP now temporarily binds the prior filter and delegates through public
`filter-buffer-substring`.
- The first boundary repair exposed a DELETE lifecycle ordering bug in the
focused test: cleanup ran while the temporary prior-filter binding was
active. The final implementation inhibits cleanup during dispatch, unwinds
the binding, then performs ownership cleanup against the real slot.
- Direct private-API scan is clean. No new abstraction, file, dependency,
compatibility shim, or silent fallback was introduced.
## Quality and Review Gate
Run full/default/permuted/isolated ERT, fuzz, portable/native/debug/sanitize
C builds, warning-as-error Elisp compilation, checkdoc, pinned
package-lint, shell/YAML/release/dictionary/static checks, GUI matrix, full
diff review, private-boundary scan, then independent `code-reviewer`
APPROVE and `architect` CLEAR.
Current post-fix evidence: default and seeded-permuted ERT 130/130; every
one of 130 ERT tests in a fresh Emacs process; C/Elisp fuzz 300/300;
warning-clean Elisp/C builds; checkdoc and pinned package-lint; release,
dictionary, shell/YAML/static checks; and live GUI matrix 7/7 with a clean
single-window screenshot. Independent code review covered 98 current paths
with zero findings and returned `APPROVE`; the independent architecture
invariant review returned `CLEAR`. `task015` is closed.
## Stop Condition
All issue/task records are closed, both independent review lanes are clean,
all gates pass from the final source state, and no remote publish/tag/push
action has been performed.

View File

@ -1,27 +0,0 @@
# Plan: Fail-Closed GUI Verification
## Scope
Resolve `issue007`: retain the explicitly loaded developer GUI matrix while
making any failed row a nonzero automation result.
## Resolution Path
1. Add a batch-safe forced-failure control for the missing assertion
boundary.
2. Preserve each case as structured data and format all rows centrally.
3. Print the full table, then exit 1 in batch mode when any row fails.
4. Document the tests-only loading boundary and automation status.
5. Run focused/full ERT, a non-default permutation, and the live seven-case
GUI matrix; inspect a clean guarded screenshot.
## Non-goals
- No headless approximation of real pixel rendering.
- No promotion of showcase/matrix code into the runtime package.
- No GUI CI provider or new dependency.
## Rollback
Restore string-only case results and table rendering, then remove the two
batch-boundary tests. No persisted data changes.

View File

@ -1,44 +0,0 @@
# Plan: Hot-loop Performance and Nil Cache 2026-07-28
## Goal
Remove only measured allocation pathologies and make cached “no hyphen”
results real cache hits without changing tokenization or hyphenation output.
## Baseline Evidence
Source-mode, three-run adversarial benchmark on Emacs 30.2:
| Length | one Latin token | dense hyphen insertion |
|---:|---:|---:|
| 1,000 | 0.304 s | 0.082 s |
| 2,000 | 0.755 s | 0.061 s |
| 4,000 | 1.202 s | 0.291 s |
| 8,000 | 3.133 s | 0.945 s |
At 8× input, tokenizer time grew about 10.3× and dense insertion about
11.5×. Both justify a linear builder. Separately, two lookups of a word
with no breaks call `ekp-hyphen--compute` twice.
## Scope
1. Add a failing call-count test for cached nil and behavior controls for
long/propertized tokenizer and dense insertion output.
2. Replace per-character accumulator concatenation with fragment lists that
concatenate once per emitted box.
3. Build inserted hyphen output from original word slices in one pass.
4. Use an explicit miss sentinel for the word-position cache.
5. Add reusable adversarial scaling output to `tests/ekp-bench.el`.
6. Re-run before/after scaling, focused/full ERT, warning-as-error byte
compilation, checkdoc, fuzz, and diff review.
## Non-goals
- Do not move tokenization or hyphenation into C.
- Do not add a rope/builder abstraction or dependency.
- Do not optimize ordinary paragraph code whose scaling is not measured.
## Rollback
Restore the string accumulators and insertion loop together. Keep the nil
cache regression test even if the builder change is reverted.

View File

@ -1,30 +0,0 @@
# Plan: Composable Buffer Integration Lifecycle
## Scope
Resolve `issue003`: compose and restore an existing substring filter, and
remove save/search/copy integrations when no justified span or auto mode
needs them.
## Resolution Path
1. Drive the public `filter-buffer-substring` path with a prior local filter
and prove EKP currently overwrites it.
2. Record the prior filter and whether it was local; define prior-filter
then logical-inversion order so DELETE remains owned by the prior path.
3. Split internal structural unjustify from the public lifecycle wrapper.
4. Derive integration removal from actual spans plus auto-mode state,
including external deletion and mode shutdown.
5. Verify copy, delete, local/global restoration, manual unjustify,
isearch, mode disable, full ERT, fuzz, compilation, and checkdoc.
## Non-goals
- No new command/menu surface.
- No general multi-filter framework.
- No save-serialization redesign beyond preserving `task004`.
## Rollback
Restore direct filter assignment and the prior public unjustify body, then
remove the composition/lifecycle tests. No persisted state changes.

View File

@ -1,36 +0,0 @@
# Plan: Interactive Protection Workflow 2026-07-28
## Goal
Make the existing no-break and verbatim workflows discoverable and
unambiguous without inventing persistence or another command layer.
## Resolution Path
1. Exercise all four protection commands through `call-interactively`.
2. Prove no-break affects the real formatter and verbatim affects the real
region formatter.
3. Give every property change explicit feedback that states its
current-buffer-session lifetime.
4. Expose the existing commands in the minor mode's standard menu and mode
help; keep the existing `fill-paragraph` remap.
5. Document removal commands and session-local lifetime in both READMEs.
## Default Behavior
Protection text properties remain local to the live buffer. Saving writes
logical text only; reopening does not restore manually applied protection.
Mode-native faces/predicates remain the persistent-source integration path.
## Non-goals
- Do not serialize text properties or change the file format.
- Do not add a transient UI, dependency, or global key binding.
- Do not infer language/mode syntax beyond the existing Org/Markdown
presets.
## Validation
Focused red/green public-command ERT, full ERT in default/permuted/isolated
orders, warning-as-error byte compilation, checkdoc, and documentation/diff
checks.

View File

@ -1,94 +0,0 @@
# Plan: Post-Audit Hardening 2026-08-20
## Context
The repository audit after `183f256` found runtime and gate defects that are
independent of the already accepted text-property layout architecture. This
plan stays inside `phase-kp-overhaul-20260726`; it does not reopen closed
issues or weaken the source-clean display contract.
## Goals
1. Make public buffer width errors fail before projection mutation and keep
the previous projection intact on every rejected request.
2. Define and test the backward-delete live projection invariant before
changing the row-crossing owner.
3. Resolve locale spellings to the exact bundled dictionary before short-code
fallback, and reject malformed C position vectors at the module boundary.
4. Restore the showcase's automatic inline-code contract and make every test
runner use current source and the complete ERT set.
5. Remove manual buffer integrations when no projection or auto-mode owner
remains.
6. Re-profile `issue018`/`task030` after the correctness fixes with a
source-fresh evaluator and close it only if the locked source-instrumented
16 ms contract is actually met.
## Scope and milestones
- M1 (`task037`, `task042`): buffer width validation, failure atomicity, and
integration lifecycle.
- M2 (`task038`): backward-delete projection semantics and a regression that
drives the public edit path.
- M3 (`task039`, `task040`): exact hyphenation locale lookup and complete C
position-vector validation, with direct boundary tests.
- M4 (`task041`): showcase automatic inline face, source-first test loading,
and complete random/isolated selection.
- M5 (`task030`, `issue028`): make the locked width/engine matrix source-fresh,
then optimize only the remaining structural commit owner.
## Non-goals
- Do not change valid KP output, the 15-field C ABI, or the source-clean
buffer representation.
- Do not hide latency with debounce, stale layout reuse, skipped publication,
global GC changes, or test-only exceptions.
- Do not close `issue018` from byte-compiled production numbers alone.
- Do not rewrite historical postmortems; append current decisions and
verification evidence.
## Acceptance gates
- Invalid or non-positive buffer widths signal before any owned property,
span, filter, hook, point, mark, or modified-state change.
- Backward deletion either preserves the committed projection as specified or
has an explicit revised contract documented and tested through the public
command path.
- `de-CH`/`de_CH` and equivalent exact locale spellings select the same exact
dictionary; unknown locales use the documented short-code fallback.
- Out-of-range, unsorted, or duplicate C hyphen/forbidden positions signal
`ekp-c-invalid-input`; valid arrays retain exact Elisp/C parity.
- Clean source, stale bytecode, normal, random, and isolated test runners
all exercise the same complete current ERT inventory and the showcase's
automatic inline contract.
- The live evaluator explicitly loads source from both baseline and candidate
roots; a bytecode-backed `pass:true` is not valid evidence.
- Full ERT, fuzz, C build/tests, byte compilation, checkdoc, package-lint,
release, dictionary, and focused performance gates pass before closure.
## Risks and dependencies
- Backward-delete behavior is a product invariant, not a local condition;
implementation must follow the existing spec and real public-path evidence.
- C position validation must match the DP's exclusive break-index domain and
preserve sorted-vector assumptions used by binary search.
- The performance gate depends on trustworthy source loading; stale ignored
`.elc` files must not influence any result.
## Stop condition
Stop only when all tasks in this plan have current issue/task evidence,
closed issues have user-visible or contract-level verification, `issue018`
is either genuinely closed by its locked gate or explicitly remains open with
fresh measurements, and the complete diff is committed and reviewed.
## 2026-09-01 Emacs 31 strict-compile follow-up
- `task044` / `issue029` replace the six remaining single-binding
`if-let`/`when-let` forms in production and the three forms in the live
evaluator with their semantically identical starred variants.
- No layout, hyphenation, font-selection, module-path, or evaluator behavior
may change. Existing ERT coverage locks those paths; Emacs 31.1 WERROR byte
compilation is the regression gate for the compatibility defect itself.
- Closure requires root integration compilation, strict production and changed
test-tool compilation, the complete normal and seeded-random ERT suites, C
build/parity fuzz, release/dictionary checks, checkdoc, and diff-check.

View File

@ -1,36 +0,0 @@
# Plan: Property-Sensitive Paragraph Fast Path 2026-07-28
## Scope
Fix `issue010` only: mutating layout-relevant text properties on an already
cached string object must not reuse stale paragraph preprocessing.
## Resolution Path
1. Warm the public paragraph resolver with the exact string object.
2. Mutate `ekp-no-break` on that object and prove the current fast path
returns stale boxes/break permissions.
3. Make the fast path compare the same complete structural paragraph key as
the hash cache instead of maintaining a partial parallel signature.
4. Remove invalidation watchers that become redundant once one key owns both
paths.
5. Verify the mutated object matches a fresh computation across CJK and
Latin-with-space inputs, then rerun every final gate.
## Ownership
`ekp--para-key` is the sole owner of paragraph preprocessing identity. The
most-recent lookup may bypass the hash table, but it may not bypass or
partially reimplement that key.
## Non-goals
- No new cache layer or eager global invalidation.
- No public API, rendering, C ABI, or serialized-data change.
- No attempt to observe arbitrary font/theme mutation; the documented
`ekp-clear-caches` boundary remains unchanged.
## Rollback
Restore the partial fast-path tuple and its variable watchers. No persisted
state needs migration, but the same-object property regression returns.

View File

@ -1,40 +0,0 @@
# Plan: Release and CI Governance 2026-07-28
## Goal
Make repository-local release evidence reproducible: CI inputs are immutable,
the supported Windows platform is exercised, `.phrase` decisions are tracked,
and one local gate checks the version and workflow invariants before a release.
## Scope
1. Add a failing static release-gate test for action pins, platform coverage,
tracked decision records, package/changelog version agreement, and the C
module version pair.
2. Pin third-party actions to full upstream commit SHAs with readable tag
comments.
3. Add a Windows batch-test job using the existing public test runner.
4. Stop ignoring `.phrase` and document the release checklist, including
remote/tag and artifact checks that cannot safely be automated locally.
5. Run the gate, YAML parse/static checks, default tests, and diff checks.
## Non-goals
- Do not push commits or tags, publish artifacts, or rewrite existing tags.
- Do not claim that an unrun remote GitHub Actions workflow is green.
- Do not add a release framework or package dependency.
## Risks and Mitigations
- **Pinned actions age:** keep the human-readable upstream tag beside each SHA
and make updates an explicit reviewed change.
- **Windows shell differences:** call Emacs directly from PowerShell instead
of depending on the POSIX test wrapper.
- **Local versus remote state:** the gate checks repository invariants; the
release checklist separately requires a clean commit, signed/annotated tag,
remote CI, checksums, and remote tag verification.
## Rollback
Revert the gate, workflow, `.gitignore`, and release documentation together.
Do not restore floating action refs independently.

View File

@ -1,48 +0,0 @@
# Plan: Repository System Audit 2026-07-28
## Context
This audit is a follow-up inside the existing `phase-kp-overhaul-20260726`.
It does not open a new phase because the requested repository-wide health
review is part of the same overhaul purpose.
## Milestones
1. Inventory the repository, architecture, public contracts, current phase,
and release state.
2. Audit Elisp core, C module, buffer/region integration, tests, GUI,
dictionaries, documentation, and release maintenance.
3. Reproduce high-risk findings and run the existing verification surfaces
from a clean source snapshot.
4. Publish a prioritized system audit with evidence, inference boundaries,
improvement directions, extension options, and explicit non-goals.
5. Record open issues and correct factual drift in public documentation.
## Scope
- Runtime source is read-only for this task.
- Documentation changes may add the audit and correct facts already proven
by the repository.
- No release, push, dependency addition, or architectural implementation.
## Evidence
- Emacs 30.2 ERT suite and C/Elisp fuzz suite.
- Clean-copy byte compilation, checkdoc, C release build, ERT, and fuzz.
- GUI fit matrix across scaling, remapping, fringe, and narrow-window cases.
- Targeted negative probes for caches, save failure, copy-filter composition,
and test isolation.
- Local/remote Git and tag inspection.
## Risks and Dependencies
- Windows and remote CI were not available locally.
- macOS sanitizer runtime policy prevented loading the debug C module.
- The GUI matrix produced numerical evidence, but the clean screenshot path
was obstructed by a macOS permission prompt.
## Stop Condition
The audit is complete when findings are prioritized and documented, factual
README drift is corrected, phase records are updated, the complete diff is
reviewed, and no runtime source file has changed.

View File

@ -1,30 +0,0 @@
# Plan: Non-mutating Save Serialization
## Scope
Resolve `issue002`: saving justified content must write logical text while
the display buffer remains unchanged on success, filesystem failure,
encoding failure, or interruption.
## Resolution Path
1. Drive `save-buffer` through real failure and `quit` paths and prove the
old before/after-hook transaction leaves the buffer unformatted.
2. Stop mutating the source buffer during save.
3. Use Emacs's `write-region-annotate-functions` buffer-switch contract to
serialize a logical copy at the actual write boundary.
4. Preserve later annotation/coding processing and clean the copy on
success, retry, integration removal, or source-buffer teardown.
5. Verify success, three failure classes, retry, full ERT, fuzz,
warnings-as-errors compilation, and checkdoc.
## Non-goals
- No global advice around `save-buffer` or `write-region`.
- No replacement implementation of Emacs file saving.
- No copy-filter composition or manual-unjustify lifecycle change.
## Rollback
Restore the before/after save hooks and remove the logical write-buffer
tests. No file format or persisted metadata changes.

View File

@ -1,29 +0,0 @@
# Plan: ERT State and Order Isolation
## Scope
Resolve `issue004`: make the parshape dispatch regression truthful, restore
all test-owned EKP configuration, and make order dependence observable.
## Resolution Path
1. Reproduce the named parshape test alone with the C module loaded.
2. Drive the public formatter with a real parshape and an observable C-call
negative control.
3. Dynamically bind every mutable EKP option and spacing state owned by the
shared clean-state fixture.
4. Add public-ERT-API runners for reproducible permutation and one fresh
Emacs process per test.
5. Run the focused matrix, full permutation, full isolation, compilation,
and checkdoc.
## Non-goals
- No test framework dependency.
- No reliance on private ERT functions.
- No production formatter change.
## Rollback
Restore the partial fixture and default runner, then remove the new
permutation/isolation entry points and CI step. No persisted data changes.

View File

@ -1,411 +0,0 @@
# Plan: Text-Property KP Layout Engine 2026-07-29
The authoritative user-visible contract is
`spec_text_property_layout_engine_20260729.md`.
## Goal
Resolve `issue011` at the representation owner. The real buffer keeps only
the user's logical characters. EKP computes Knuth-Plass breaks, glue widths,
indentation, and discretionary hyphens as a reusable layout plan, then a
buffer renderer projects that plan with text properties on existing source
characters.
No buffer path may insert layout spaces, soft newlines, or discretionary
hyphens. No buffer path may use overlays.
## User Contract
- `buffer-string`, `buffer-substring-no-properties`, direct character
access, search, syntax, markers, point, save, and ordinary editing observe
the original character sequence.
- Manual commands produce the same KP break and glue decisions as the
string API.
- `ekp-auto-justify-mode` keeps the current unfinished visual row natural and
preserves unaffected committed break anchors during continuous editing.
- Ordinary same-row edits do not call whole-hard-line planning. A dirty edit
transaction owns the local natural island and a committed projection owns
the saved source, plan, signatures, spans, and anchors.
- Native soft-wrap crossing atomically replans the completed hard-line prefix
once. Signature diffing limits the property update; core DP remains the
owner of the committed result.
- Hard newline/paragraph completion, the next real edit outside the dirty
island, explicit refill, and width/font/layout-context change are commit
events. Point-only motion is never one.
- Reversible source edits restore the saved projection exactly rather than
relying on a recomputation to happen to reproduce it.
- Layout-property updates do not change the modified flag, undo history,
character-modified tick, point, mark, or external modification hooks.
- Turning the mode off removes only EKP-owned projection properties and
restores every pre-existing property exactly.
- Final-pass emergency layout remains owned by core K-P. Ordinary underfull
candidates use finite emergency stretch and normal badness/demerits; the
final active path is preserved with TeX-style zero-increment artificial
demerits only when an overfull candidate would otherwise extinguish it.
The buffer renderer and policy compiler must not add CJK-orphan, unit,
hard-atom-adjacency, or screenshot-specific compensation.
## Architecture
### Core owner: `ekp.el`
Introduce one semantic paragraph plan built from the existing `ekp-para`,
DP breaks, line glues, and source box offsets. A plan contains ordered
lines; each line records its source/box bounds, leading indentation,
interior glue targets, chosen break, and discretionary-hyphen state.
The plan contains no buffer positions, markers, overlays, or display
property forms.
`ekp-pixel-justify` remains the public string formatter. Its renderer
consumes the plan and preserves the existing reversible string output and
tests. The buffer renderer consumes the same plan but has different
representation rights.
### Buffer owner: `ekp-buffer.el`
The buffer renderer maps plan operations to existing source characters:
- ASCII source-space glue:
`((space-width FACTOR) (min-width ((TARGET))))`.
- Zero-source CJK/mixed glue: `min-width` on the preceding complete
grapheme, with a target equal to the grapheme's natural width plus glue.
- Leading indentation/alignment: `line-prefix` over the complete planned
display line.
- Break at source whitespace: the first break-space displays as a newline;
remaining boundary whitespace displays as empty.
- CJK break: the preceding complete grapheme displays as
`GRAPHEME + NEWLINE`.
- Latin discretionary break: the preceding complete grapheme displays as
`GRAPHEME + HYPHEN + NEWLINE`, with cursor anchoring on the reproduced
grapheme.
- Paragraph-edge whitespace stripped by the KP model remains in the buffer
and displays as empty.
All projection properties are installed through `with-silent-modifications`
and are nonsticky. Copy/kill strips only EKP projection metadata so stale
layout cannot be yanked elsewhere.
### Display ownership
EKP never overwrites an unowned replacing `display`, `line-prefix`, or
`wrap-prefix` value. A paragraph with a property that cannot be composed
losslessly is kept verbatim and reported by diagnostics. Removing a layout
restores the exact pre-existing property values.
### Multiple windows
Text properties are buffer-wide. One buffer therefore has one
authoritative plan: the narrowest live window showing that buffer, matching
the current `ekp-buffer--effective-width` behavior. Wider windows may have
unused right-side space; no window may receive an overflowing plan.
Simultaneous different KP plans for one buffer are explicitly not claimed.
## Live Editing State Machine
1. The active hard line owns one committed projection: baseline source,
whole-hard-line plan, line signatures, projected spans, and anchors.
2. The first edit in one visual row snapshots that state and opens a dirty
island. `before-change` removes only the affected projection; it never
clears an unrelated suffix.
3. Same-row edits update source and dirty bounds only. Existing glue and
native soft wrapping absorb local edits without whole-hard-line DP.
4. Restoring the baseline source restores the saved owned properties and
committed state immediately and closes the transaction.
5. Crossing a native soft-wrap boundary commits once: recompute or reuse the
whole-hard-line plan, derive the completed prefix, and apply the
signature-diff update as one silent publication. The new current row is
natural.
6. A real edit outside the dirty island commits the old transaction before
opening a new one. Hard newline/paragraph completion, explicit refill, and
width/font/layout-context changes are also commit boundaries.
7. Point-only motion anywhere is display read-only and cannot commit,
finalize, plan, touch cache identity, or write projection properties.
8. IME composition stays entirely native until commit. Stale generations,
foreign display ownership, unsupported shrink, oversized hard lines, or
projection failures fail closed to native display for that hard line.
9. Hard-paragraph completion uses one existing full KP quality pass and
starts a new natural active hard line.
## Milestones and Gates
### M1 — Core layout plan and string parity
- Add plan structs and `ekp-layout-plan`.
- Make the string renderer consume the plan.
- Gate: every existing core rendered string remains
`equal-including-properties`; C/Elisp parity and fuzz remain green.
### M2 — Static text-property buffer renderer
- Replace delete/insert justification with property projection.
- Remove physical inversion/save/isearch adapters that no longer own a
character transformation.
- Gate: source characters, positions, modified state, undo, hooks, and
foreign properties are invariant; exact GUI glue/break/hyphen/indent
probes pass.
### M3 — Seamless live-editing foundation
- Implement composition deferral, resize generation cancellation,
visible-first large-buffer work, the single-paragraph planning guard, and
exact editor-state preservation.
- Gate: source, point/mark, whitespace, IME, resize, and teardown regressions
are locked before selecting the final live row-boundary model.
### M4 — Product and repository closure
- Update bilingual user/developer documentation, `issue011`, task/change
records, changelog, and a design postmortem.
- Run default/permuted/isolated ERT, fuzz, byte compilation with warnings as
errors, checkdoc, C parity/build gates, GUI matrix, dynamic verification,
anti-slop cleanup, and independent architecture/code review.
### M5 — Interaction regression closure
- Preserve the mark marker and `mark-active` as independent editor state
across every reprojection, including showcase width changes.
- Keep active-line edge whitespace natural while retaining edge cleanup on
committed static lines.
- Gate: focused red/green ERT, a public-command interaction matrix, clean
GUI width-key/single-space evidence, full repository gates, and user
confirmation for `issue012` and `issue013`.
### M6 — Native progressive editing
- Delete the partial-KP live-flow model and its lookahead/convergence state.
- Keep the active source tail under native Emacs redisplay ownership.
- Align only completed native screen rows without live break/hyphen
projection; run complete KP at hard-paragraph completion.
- Own native soft wrapping while auto mode is active, including narrow
partial-width windows, and restore the previous display-variable
ownership on teardown.
- Gate: per-keystroke mixed-text GUI evidence, backward-edit invalidation,
paragraph-completion transition, narrow split-window soft wrapping, full
repository gates, and user confirmation for `issue014` and `issue015`.
Historical note: M6 is superseded for live planning by M7. Its active-tail
and soft-wrap lifecycle decisions remain prerequisites, but native visual
rows are not the durable planning unit.
### M7 — Semantic hard-line prefix editing
- Status: implementation, automated/performance/GUI gates, and independent
code/architecture reviews are complete. `issue016` remains open for the
user's visible editing confirmation.
- Replace native-row commitment with whole-hard-line plan consumption in
`ekp-buffer`.
- Keep `ekp.el`, core DP semantics, C ABI, DP schema, and
`ekp-layout-plan` contracts unchanged.
- Project only complete semantic plan lines before point; keep the
point-containing plan line and all following source natural.
- Add buffer-local history cache entries keyed by hard-line text,
text-property/layout context, authoritative width, font/face/text-scale
context, and EKP layout parameters.
- Use semantic line signatures to avoid rewriting unchanged prefixes and
to prove later edits can revise earlier breaks/glue together.
- Gate: public-path red/green ERT, GUI dynamic recording, latency/cache
benchmark, full repository gates, independent review, and user
confirmation for `issue016`.
### M8 — Source-edit-owned live frontier
- Status: superseded by M10 for edit-trigger ownership. The zero-work
point-motion invariant remains current; `issue017` remains open for the
user's visible confirmation.
- Preserve the latest real source-edit position as the live frontier.
- Make point-only motion within the active hard line perform no DP, cache,
or text-property work.
- Preserve the frontier across width/font/layout-context reflow and map it
into the resulting whole-hard-line plan.
- Gate: focused property-identity RED/GREEN ERT, public earlier-line edit,
reflow-after-motion regression, full repository gates, dynamic GUI
point-motion evidence, independent review, and user confirmation.
### M9 — Narrow unique-append latency
- Status: re-profiled as `issue018`/`task030` after M10; ready for work.
Ordinary same-row planning is gone, so only structural-commit spikes are
in scope.
- Establish a repeatable width/length matrix before optimizing.
- Reduce new-source-state planning cost without stale plan reuse,
debounce, skipped publication, global GC changes, or weaker layout
semantics.
- Keep task029's point-motion zero-work invariant as a permanent gate.
### M10 — Stable live projection transactions
- Status: complete as `issue019`/`task031`; automated, static, benchmark,
and reviewed temporal GUI gates pass. `issue019` remains open only for
user-visible confirmation.
- Replace frontier-owned native suffix invalidation with a committed
projection baseline and one persistent dirty edit transaction.
- Preserve unaffected break anchors during middle-line edits; use native
soft wrapping for local word migration.
- Replan only at soft-wrap or structural commit boundaries and publish the
changed prefix atomically.
- Restore reversible edits exactly from the saved baseline.
- Preserve core DP/C ABI/schema/plan contracts and the point-motion
zero-work invariant.
- Gate: focused RED/GREEN public-path ERT, complete repository gates,
re-profiled live benchmarks, and temporal GUI evidence.
### M11 — C-backed resize latency
- Status: `task032` developer-complete; `issue020` awaits user confirmation.
- Freeze the current portable module and measure an interleaved
baseline/candidate width-and-length matrix with raw p50/p95 evidence.
- Attribute complete resize time across plan construction, Emacs/C
marshalling, C DP, and projection publication before selecting a change.
- Require exact frozen-C and Elisp layout parity; no stale width reuse,
debounce inflation, skipped reflows, approximate planning, or global GC
workaround is acceptable.
- Gate: at least 20% paired p50/p95 improvement, candidate p95 at or below
50 ms, complete automated/static gates, and temporal GUI resize evidence.
- Result: core p50/p95 improved by 33.25%/34.09% to 15.318/27.687 ms;
complete resize improved by 43.51%/41.03% to 15.900/27.487 ms. Exact
frozen-C/Elisp parity and all automated, static, and GUI gates pass.
### M12 — Rigid inline atom emergency breaking
- Status: `task033` implemented and developer-verified as the earlier
rigid-atom repair. `task036` supersedes its ordinary-underfull emergency
model; `issue021` was closed after user-visible confirmation.
- Reproduce the showcase's narrow CJK prefix plus `ekp-no-break` atom through
the public string and semantic-plan paths before changing the algorithm.
- Historical task033 design: keep the strict K-P pass and the established
first-candidate emergency transition unchanged, then add a narrow
forbidden-run prefix fallback. The current task036 model replaces that
fallback with fixed final-pass emergency stretch plus content-independent
active-path preservation.
- Preserve atom integrity, source text, Elisp/C parity, looseness/parshape
semantics, append correctness, and the buffer projection contract.
- Gate: focused RED/GREEN core and buffer regressions, complete ERT in normal
and random order, isolated tests, C/Elisp fuzz, warning-as-error builds,
static/release checks, and clean static plus dynamic 280px GUI evidence.
- Result: the failing boundary moved from 1 to the full prefix boundary 11 in
Elisp, C, and public buffer paths. Normal/random/isolated ERT pass 201/201,
fuzz passes 300/300, static and build gates pass, and reviewed
480→280→340→280 GUI evidence returns PASS.
### M13 — Configurable break policies and orphan-glyph closure
- Status: `task034` implementation, focused automated evidence, and G004 GUI
evidence are recorded. G005 repository-wide gates and G006 cleanup are
complete. `task034` is closed for developer work, `issue021` is closed
after user visual confirmation, and independent final code review returned
APPROVE with architecture status CLEAR.
- Implement the locked A2/B2/C2/D1/E1/F1/G2/H1/I1/J1/K1/M1 contract:
inline code defaults to no-hyphen rather than no-break, known inline faces
are recognized through mode profiles, region policy outranks explicit
local values which outrank mode profiles and globals, block faces remain
verbatim, explicit no-break never downgrades, automatic no-break downgrades
to no-hyphen when overwide, inline and block faces use separate paths,
manual properties stay session-only, hyphenation defaults to auto,
URL/path/identifier default to no-hyphen, compact number-unit defaults to
no-break, kinsoku defaults to common, overlong tokens default to emergency,
and buffer measure defaults to the narrowest live window.
- Keep the chosen architecture: resolve private structural policy intervals
before tokenization, compile them into existing hyphen positions and
forbidden-break vectors before DP, preserve the original source in the
semantic plan, and keep the C boundary at the current 15 arguments and
15-field batch payload unless architecture review is reopened.
- Split block and inline ownership in `ekp-buffer`: `ekp-buffer-skip-faces`
remains paragraph-level verbatim, `ekp-buffer-inline-faces` annotates only
exact inline intervals, and `ekp-buffer-mode-policy-alist` is consulted
without auto-copying profile values into buffer locals.
- Add public region controls for `ekp-break-policy`: normal, enable
hyphenation, disable hyphenation, and clear. Existing no-break/verbatim
commands remain the only hard-atom and paragraph-bypass controls.
- Fix the remaining visible quality bug from the user's latest screenshot:
the 280px showcase path must not isolate any pathological single-CJK source
line around inline code, including `行`, `内`, or `永`, when a legal
non-emergency alternative exists.
- Gate: follow the RALPLAN test specification R1-R8 plus GUI verification.
Required evidence includes focused RED/GREEN ERT, full normal/permuted/
isolated ERT, 300-case property fuzz, C/Elisp parity, warning-as-error
Elisp compilation, C builds/tests, checkdoc/package/static/release gates,
and reviewed fullscreen single-window screenshots for inline wrapping,
no orphan glyph, explicit no-break, block verbatim, measure modes, and
overlong-token modes.
- Current evidence: G002 records core policy compilation, C parity, 300-case
fuzz, byte compilation, checkdoc, C build, and focused policy/cache gates.
G003 records buffer/profile/local/region controls, diagnostics, generation
reflow, source/editor invariants, focused core/buffer suites, byte
compilation, and checkdoc. G004 records GUI verifier ERT 7/7 and a clean
single-window run at `/tmp/ekp-g004-evidence.Tp77dW` with 12/12 checkpoints,
no failed assertions, 25.75s/206-frame recording, no black segments,
automatic inline wrapping 3→2→3 lines across 280→340→280, source-space
internal inline breaks, explicit hard atom, block verbatim, C active, and
no stale policy projection after the no-hyphen→normal transition.
- Final evidence: G005 recorded repository-wide default/seeded/isolated ERT,
property fuzz, warning-clean Elisp/C builds, checkdoc/package/release/
dictionary/static gates, performance checks, and full diff review. G006
cleanup touched only `ekp.el` and `ekp-buffer.el`; targeted cleanup suites
passed 6/6 and 3/3, full core ERT passed 125/125, full buffer ERT passed
120/120, and byte compilation, diff-check, and C build gates passed. G006
was then review-blocked on nested semantic-plan cache aliasing and malformed
non-nil C fallback semantics. G007 resolved those first blockers: cached
semantic plans return consumer-owned copies of plan-owned mutable payloads
while intentionally sharing `para`; malformed non-nil C single/batch output
signals `ekp-backend-contract-error`, with nil-result Elisp fallback and
the 15-argument/15-field ABI unchanged. G007 final review found one
remaining string-leaf context alias; G008 resolves it by recursively
copying cons/vector/string context payloads both for snapshot/cache-key
creation and returned plans. Fresh G008 gates record formal resize parity
true, ERT 268/268, fuzz 300/300, release pass, core p50/p95
23.266/36.363ms, resize p50/p95 23.761/36.889ms, and byte-compiled current
public path p99 below 16ms with zero GC. After review remediation made all
changed tests warning-clean and synchronized the eighth C setter parameter,
final independent code review returned APPROVE and architecture review
returned CLEAR.
### M14 — Final-pass emergency stretch
- Status: `task035` was falsified; `task036` is implemented and its developer
gates pass. `issue021` is closed after user-visible confirmation.
- Correct the core K-P owner, not the renderer or policy compiler. Strict
pass behavior remains unchanged.
- In the final pass, ordinary underfull candidates receive finite background
emergency stretch and still compute adjustment ratio, badness, fitness, and
demerits. Separately, when an overfull candidate would otherwise eliminate
the final active path to a breakpoint and no non-overfull candidate survives
there, install the best provisional path with tight fitness and zero
incremental demerits, matching TeX's `artificial_demerits` purpose.
- Do not add CJK-orphan, unit, or screenshot-specific penalties. Unit suffix
configuration remains only a token-classification input, not a layout
scoring rule.
- Gate: focused core/public-buffer/C parity tests and GUI-oracle checks must
reject any isolated CJK source line in the showcase paragraph at checked
widths. Final fullscreen visual review remains required before closing
`issue021`.
- Result: the Elisp 1D, looseness/parshape, and C paths share the same rule;
explicit atom interiors remain unbreakable but adjacent legal boundaries
remain legal; the public 15-field/15-argument C contract is unchanged.
Focused regressions pass 8/8, the emergency selector passes 10/10, the
core/buffer/GUI oracle passes 8/8, full ERT passes 288/288, seeded and
isolated core suites pass 279/279, fuzz passes 300/300, all build/static/
release gates pass, performance gates pass, and reviewed dynamic GUI
evidence at `/tmp/ekp-g009-evidence-retry.UOpPNp` returns PASS.
## Stop Gates
- Stop the affected paragraph instead of stealing a foreign replacing
display owner.
- Stop exact shrink projection for tabs or non-ASCII whitespace when
`space-width` cannot express it; keep that paragraph verbatim and report
why.
- Stop if the live-edit implementation appears to require a core DP, C
ABI, DP schema, or `ekp-layout-plan` semantic change; that means the
buffer/core boundary has been crossed incorrectly.
- Stop and return to diagnosis after two failed fixes for the same display
or live-edit invariant.
- Do not claim completion while any source-character, undo, modified-state,
property-restoration, GUI, or dynamic-edit invariant is unproved.
## Rollback
The string renderer can be restored to the current direct implementation
because M1 preserves its public result. The buffer renderer can be reverted
to the last released physical representation only as a full rollback; no
compatibility shim or mixed physical/property backend will be retained.

View File

@ -1,244 +0,0 @@
# Spec: Source-Clean Text-Property KP Layout 2026-07-29
## Summary
`ekp-buffer.el` presents Knuth-Plass layout without rewriting the buffer's
character stream. The core computes a reusable semantic layout plan;
manual buffer commands and `ekp-auto-justify-mode` project that plan using
text properties on existing source characters only.
The character sequence remains the document. Visual glue, breaks,
indentation, and discretionary hyphens are display metadata: EKP creates no
overlay and inserts no layout character.
## Goals
1. Keep `buffer-string`, `buffer-substring-no-properties`, direct character
access, search, syntax, markers, saving, and ordinary editing on the
original character sequence.
2. Use the same core glue, break, indentation, and hyphen decisions for the
compatible string renderer and the non-mutating buffer renderer.
3. Make automatic layout stable while editing: committed prefix lines keep
their projection until a structural commit, the current unfinished visual
row stays under Emacs redisplay ownership, and a middle-line edit
naturalizes only its local dirty island while preserving unaffected break
anchors.
4. Preserve undo, modified state, character-modified tick, point, mark,
copy/kill, isearch, IME composition, narrowing, and mode lifecycle.
5. Fail closed around display ownership: unsupported or conflicting hard
paragraphs remain naturally displayed and are diagnosable.
## Non-Goals
- Hiding EKP metadata from Lisp that explicitly inspects text properties.
The clean-text guarantee concerns the character sequence; copy/kill strips
EKP-owned projection metadata.
- Independent simultaneous layouts for different-width windows showing the
same buffer. The narrowest live window is authoritative.
- Shrinking tabs or non-ASCII whitespace with `space-width`.
- Claiming verified bidi, Arabic, Hebrew, Indic shaping, ligatures, or
arbitrary multi-codepoint grapheme behavior beyond the tested LTR
Latin/CJK boundary.
- Retaining `(require 'ekp-region)` or old private/configuration names.
## User Flows
### Load and use buffer commands
1. The user adds the package to `load-path`.
2. The user loads `(require 'ekp-buffer)`.
3. `M-x ekp-justify-region` projects the active region, or the hard
paragraph at point when no region is active.
4. `M-x ekp-justify-buffer` projects the accessible buffer.
5. `M-x ekp-unjustify-region` or `ekp-unjustify-buffer` removes only
EKP-owned projection properties; source characters do not need
restoration.
### Edit with automatic layout
1. The user enables `M-x ekp-auto-justify-mode`.
2. A committed projection records the complete source baseline, one
whole-hard-line `ekp-layout-plan`, line signatures, projected spans, and
break anchors. Core DP receives no point, marker, window, or edit-state
input.
3. Ordinary input within the current native visual row performs no
whole-hard-line planning. Previously committed rows remain
property-identical and the current row stays natural.
4. Editing a committed middle row starts a dirty transaction. Only the
affected local island becomes natural; following and preceding unaffected
break anchors remain installed. Native wrapping absorbs local push/pull.
5. Crossing into the next native visual row atomically replans the completed
hard-line prefix once. Line-signature diffing limits property writes; the
new current row remains natural.
6. A hard newline/paragraph end, the next real edit outside the dirty island,
explicit refill, or width/font/layout-context change commits the
transaction. Point-only motion anywhere performs zero plan, cache,
property, or layout work.
7. Restoring the baseline source restores the saved projection immediately
and exactly, including text properties, plan signatures, and spans.
8. Completed paragraphs use the existing full Knuth-Plass projection.
9. Disabling the mode cancels pending work, detaches markers, restores any
prior copy filter, and removes every EKP-owned projection.
### Use logical text during projection
1. Search and isearch operate directly on source characters, including
across display-only breaks and discretionary hyphens.
2. Save and `write-region` serialize source characters; visual layout never
reaches disk.
3. Copy/kill composes with an existing
`filter-buffer-substring-function`, removes only EKP-owned projection
metadata from the copied string, and preserves delete semantics.
4. Lisp that needs a property-free string uses
`buffer-substring-no-properties`; property-aware APIs may inspect the
EKP display metadata without seeing synthesized layout characters.
### Diagnose an unsupported paragraph
1. EKP detects foreign `display`, `line-prefix`, `wrap-prefix`,
`composition`, or `invisible` ownership, or an exact shrink request for
unsupported whitespace.
2. EKP leaves only that hard paragraph natural instead of stealing or
approximating the foreign representation.
3. `M-x ekp-diagnose` reports the authoritative width and recorded
conflicts. The user may remove the conflict or explicitly refill after
changing the content/configuration.
## Display Contract
- ASCII source spaces use:
```elisp
((space-width FACTOR)
(min-width ((TARGET-PIXELS))))
```
- A zero-source CJK/mixed gap applies `min-width` to the preceding complete
grapheme, with a target equal to its natural advance plus the planned
glue.
- Indentation uses `line-prefix`.
- A source-whitespace break displays the first boundary character as a
newline and any remaining boundary whitespace as empty.
- A CJK or Latin discretionary break uses a replacing display string on an
existing complete grapheme. It reproduces that grapheme, appends the
optional hyphen, then a visual newline.
- Every public projection value has an EKP owner property. Cleanup removes
a public value only while it is still identical to the owned value.
- All projection updates use `with-silent-modifications`; owned properties
are nonsticky.
## Edge Cases
- Active IME composition defers projection and retries only for the current
generation.
- Stale resize/background generations cannot publish.
- A hard paragraph over `ekp-auto-justify-paragraph-limit` remains natural
during automatic work; explicit `ekp-refill-paragraph` is unbounded.
- Large buffers reflow visible-first in time-bounded hard-paragraph chunks.
- Foreign property conflicts are isolated to their hard paragraph.
- Narrowing does not cause projection cleanup or installation outside the
intended accessible operation; mode teardown widens to remove all owned
state.
- Major-mode changes and mode disable leave the logical text and prior
integrations intact.
- A rigid inline atom that jumps a candidate line from underfull to overfull
stays intact without forcing the preceding prose through one-box emergency
lines. In the final pass, ordinary underfull candidates receive finite
emergency stretch and remain normal K-P cost candidates. An atom wider than
the measure stays intact but may share an overflow line with preceding
ordinary content when TeX-style artificial demerits must preserve the last
active path. Atom adjacency has no special break or scoring rule.
- Inline code is not rigid by default. Known Org/Markdown inline faces use
the inline policy path, not the paragraph verbatim path. The default policy
permits legal wrapping at existing boundaries, suppresses discretionary
dictionary hyphenation, and preserves source whitespace literally. Known
code-block faces remain paragraph-level verbatim.
- Manual `ekp-no-break` is the only explicit hard-atom owner and is never
downgraded. Automatic no-break spans, such as compact number-unit tokens,
downgrade to no-hyphen when wider than the effective measure.
- The previous narrow showcase orphan-glyph failure is now a permanent
acceptance invariant: the inline-code paragraph must not isolate CJK glyphs
such as `行`, `内`, or `永` as one-character source lines around automatic
inline code when a non-emergency legal alternative exists. Dynamic evidence
must distinguish automatic inline code from explicit hard atoms and block
verbatim spans.
- The GUI oracle rejects any isolated CJK source line in the showcase
paragraph at the checked widths. This is a core K-P invariant, not a
unit-suffix, screenshot-specific, or renderer compensation rule.
- Effective policy precedence is deterministic: region properties, then
explicit buffer/file/dir-local values, then major-mode profiles, then
global defaults. Manual text properties are session-local; persistence
comes from mode syntax/profiles and file/dir locals.
- URL, path, and identifier tokens default to no-hyphen. Compact number-unit
tokens default to no-break. Hyphenation defaults to `auto`, with `on` and
`off` overrides. Kinsoku defaults to `common`, with `zh`, `ja`, `off`, and
custom additions available. Ordinary overlong tokens default to the current
emergency output, with `overflow` and `natural` alternatives.
- The buffer measure defaults to the narrowest live window. A positive
integer fixed measure and `(max . PIXELS)` cap are configurable and must be
reported by diagnostics when they create overflow risk.
- Reprojection preserves point, the mark marker, and `mark-active`
independently. An inactive historical mark must never become a visible
selection merely because width or layout options changed.
- Semantic live-prefix editing still requires real Emacs soft wrapping for
the active natural suffix.
`ekp-auto-justify-mode` temporarily disables both `truncate-lines` and
narrow partial-window truncation, then restores their prior values and
local/global binding state when the mode ends.
## Acceptance Criteria
1. `ekp.el` exposes a semantic layout plan containing source offsets,
per-line glue targets, break kinds, indentation, and discretionary
hyphen decisions; the public string API remains
`equal-including-properties` compatible.
2. No EKP buffer path creates an overlay or inserts glue spaces, soft
newlines, or discretionary hyphens into source text.
3. ASCII gaps use the exact combined `space-width`/absolute-pixel
`min-width` form; zero-source gaps, indentation, breaks, and hyphens use
the display contract above.
4. Source characters, positions, point/mark, modified state, undo history,
character-modified tick, save output, search, and syntax remain logical.
5. Automatic editing proves committed-projection and dirty-transaction
ownership: ordinary same-row edits perform no whole-hard-line planning or
unaffected property writes; middle-row edits preserve unrelated anchors;
reversible edits restore the complete projection
`equal-including-properties`; soft-wrap crossing publishes one atomic
whole-hard-line prefix update; structural commit events use the existing
`ekp-layout-plan`; and point-only motion performs zero layout work. IME
deferral, stale resize rejection, and exactly one complete KP pass after
hard-paragraph completion remain required. Narrow side-by-side windows
soft-wrap rather than horizontally scroll. A single leading/trailing
space or tab is visible in the same command-loop turn, including
whitespace exposed by deletion.
6. Copy/kill, isearch, foreign ownership, multiwindow narrowest-width
policy, narrowing, resize, major-mode change, and teardown have
deterministic tests.
7. Exact GUI probes cover 164px ASCII and zero-source CJK glue,
display-only break/hyphen/indentation, point, vertical motion, region,
mouse, scaling/remapping/fringes, unchanged source, and zero overlays.
8. Dynamic per-keystroke evidence proves mixed Latin/CJK same-row typing
preserves the committed projection, a real visual-row crossing atomically
publishes all completed rows from one hard-line plan, exact source
reversal restores the baseline projection, point-only motion preserves
every state object and property exactly, and paragraph completion performs
the full completed-paragraph KP transition.
9. Default, permuted, and isolated ERT; C/Elisp fuzz; warning-as-error
Elisp/C builds; checkdoc; package/static/release gates; full diff review;
independent code review; and independent architecture review pass.
10. Elisp, C, semantic-plan, string-renderer, and real GUI paths keep an
`ekp-no-break` atom intact while proving that its preceding CJK prefix is
not fragmented into one-glyph lines at narrow measures.
11. Inline and token break policies are configurable at global, mode profile,
explicit local, and region scopes. The accepted defaults are: inline code
`no-hyphen`; block code verbatim; URL/path/identifier `no-hyphen`;
compact number-unit `no-break`; hyphenation `auto`; kinsoku `common`;
overlong token `emergency`; buffer measure `narrowest-window`.
12. Region `ekp-break-policy` supports `normal`, `hyphenate`, and
`no-hyphen`, and never creates a second hard-atom representation.
Overlapping explicit `ekp-no-break` wins over every new policy.
13. The showcase paragraph containing inline code, CJK prose, NBSP-backed
numbers, and units must have no pathological single-CJK source line in
the inspected 280px GUI path while retaining exact source text, zero
overlays, block-code verbatim display, internal source-space inline
breaks, and C/Elisp plan parity.

View File

@ -1,92 +0,0 @@
# Tasks: Post-Audit Hardening 2026-08-20
- task044 [x] Restore Emacs 31.1 WERROR compilation by replacing obsolete
single-binding `if-let`/`when-let` forms with behavior-identical
`if-let*`/`when-let*` forms in production and the live evaluator.
- Source: `issue029` and the C1a nine-repository strict-compile gate.
- Validation: root `make`; Emacs 31.1 WERROR production and changed-tool
compilation; normal/random ERT 296/296; C build and 300-case fuzz;
release, local 49-entry dictionary manifest/hash, checkdoc, and diff gates.
The network-backed fixed-upstream dictionary fetch was explicitly excluded
by the user and is not claimed as evidence.
- task043 [x] Restore the Emacs 31 fresh-source baseline without weakening
cache-ownership or marker-noninheritance contracts. Keep policy fixtures
mutable and multibyte, and install EKP marker properties in the global
`text-property-default-nonsticky` default used by real buffers.
- Source: Emacs 31 fresh-source failures in the M0a baseline.
- Validation: focused ERT 3/3, normal and seeded-random ERT 296/296,
source-load, release, and 49-entry dictionary gates.
- task037 [x] Validate public buffer widths before mutation and make rejected
`ekp-justify-region` requests failure-atomic. Add red tests for zero,
negative, non-integer, and projection-preservation cases.
- Source: post-audit buffer probe and `plan_post_audit_hardening_20260820.md`.
- Validation: focused buffer ERT 4/4 plus source-preferred full ERT 296/296.
- task038 [x] Resolve backward-delete live projection semantics. Add a public
edit-path regression at the narrowest reproducible width and update the
live-layout spec/postmortem with the accepted invariant.
- Source: `postmortem/20260730-incremental-live-append-ownership.md` and
`plan_post_audit_hardening_20260820.md`.
- Validation: focused live-edit ERT 4/4, randomized ERT 296/296, and
source-first isolated ERT 294/294 process runs plus focused isolated
coverage of the later batch-position test.
- task039 [x] Make hyphenation locale resolution try the exact normalized
locale registry before short-code fallback. Add equivalent BCP-47/underscore
tests and preserve fail-closed unsupported-pattern behavior.
- Source: `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`, and
the post-audit locale probe.
- Validation: focused dictionary ERT 11/11, manifest check, and full ERT
296/296.
- task040 [x] Validate C hyphen and forbidden position vectors for the exact
exclusive break-index domain, monotonic ordering, and duplicate policy
before DP access. Add direct single/batch ABI regressions without changing
the 15-field schema.
- Source: `plan_c_api_contract_20260728.md` and the post-audit C probe.
- Validation: C warning-clean build, direct/batch C ERT 15/15, fuzz
300/300, and Elisp parity.
- task041 [x] Restore the showcase automatic inline-code face and make normal,
random, and isolated runners source-fresh and complete over the ERT
inventory. Add a regression proving stale ignored bytecode cannot mask the
showcase contract.
- Source: `888a401`, `plan_test_isolation_20260728.md`, and the post-audit
clean-source failure.
- Validation: source loader symbol checks, clean-source showcase 3/3,
normal/random/isolated 296-test inventories, and GUI contract checks.
- task042 [x] Remove manual buffer integrations when no spans remain and
auto-mode does not own the lifecycle. Preserve composition with foreign
filters and add empty/foreign-only teardown regressions.
- Source: `plan_integration_lifecycle_20260728.md` and the post-audit
no-projection probe.
- Validation: focused integration ERT 4/4 and full source-first buffer
suite.
- task030 [x] Re-profile and, if still necessary, optimize the remaining
source-instrumented narrow live-append structural commit path after
task037task042. Keep exact parity, source-clean projection, and zero-work
point motion as hard constraints; close only on the locked 16 ms gate.
- `issue028` is part of this task: each baseline/candidate evaluator round
must explicitly load source files from its own code root, never rely on
local `.elc` precedence.
- The evaluator now records `append_ms` and `append_dp_ms` and preserves
nonempty raw JSONL per run. Current source-fresh p95 is 17.161/1.187 ms
for C append/append-DP and 73.293/57.063 ms for Elisp; choose a
production optimization only after this attribution remains stable.
- It now also records append preparation/assembly separately: latest p95
is 9.753/5.663 ms for both engines, while Elisp append-DP is 55.970 ms
and C append-DP is 1.015 ms. The evaluator wrapper collision was fixed and
the same source matrix passes the harness without argument errors.
- User-selected architecture option 2 is implemented as
`ekp-auto-justify-native-append`: loaded C DP is used only for prepared
auto live append, while full/string `ekp-use-c-module=nil` remains pure
Elisp and unavailable native modules fall back exactly. The source
append-DP p95 drops from roughly 56 ms to roughly 2.2 ms in the complete
all-width/row run; preparation and assembly remain exact. The formal
four-round source-fresh evaluator passes with width-80 C p95/p99
12.010/14.622 ms and Elisp-configured live p95/p99 10.980/11.194 ms,
closing the locked 16 ms gate.

View File

@ -1,926 +0,0 @@
# Tasks: Repository System Audit 2026-07-28
- task001 [x] Produce a repository-wide audit covering architecture, Elisp,
C, buffer integration, tests, GUI, dictionaries, performance,
compatibility, documentation, release health, and extension directions.
- Source: user request; existing `phase-kp-overhaul-20260726`.
- Output: `Docs/REPOSITORY_AUDIT_20260728.md`.
- Validation: 94/94 ERT, 300/300 C/Elisp fuzz, clean-copy byte compilation,
checkdoc, warning-clean C release build, clean-copy ERT/fuzz, GUI fit
matrix 7/7, targeted negative probes, link/stale-fact/diff checks.
- Impact: documentation and planning records only; no runtime behavior
changed.
- task002 [x] Complete the DP cache signature so algorithm-parameter changes
never reuse stale results.
- Source: `issue001`, audit P1-01, and
`plan_cache_signature_20260728.md`.
- Red test: parameter-by-parameter cached-vs-fresh comparison through
`ekp-dp-cache`, plus a same-signature cache-hit assertion.
- Validation: focused red/green ERT, complete ERT, C/Elisp fuzz,
warning-clean byte compilation, checkdoc, and full diff review.
- Impact: `ekp.el`, `tests/ekp-tests.el`, user documentation, and phase
records.
- Completed: 2026-07-28; focused 0/2 → 2/2, full ERT 96/96, fuzz
300/300, byte compilation/checkdoc clean, Elisp/C rendered-output
probes matched fresh computation.
- task003 [x] Complete automatic spacing identity across both paragraph
cache lookup paths.
- Source: `issue001`, audit P2-01, and
`plan_auto_spacing_signature_20260728.md`.
- Red test: change `ekp-default-cws-stretch-pixel` without clearing
caches and exercise the paragraph hash and `ekp--last-para` separately;
retain an unchanged-signature hit control.
- Validation: focused 1/3 → 3/3, complete ERT 99/99, C/Elisp fuzz
300/300, warning-clean byte compilation, and checkdoc.
- Impact: `ekp.el`, `tests/ekp-tests.el`, public/developer documentation,
audit status, phase records, and a design postmortem.
- Completed: 2026-07-28; `issue001` is resolved.
- task004 [x] Make justified-buffer saving non-mutating and failure-safe.
- Source: `issue002`, audit P1-02, and
`plan_save_transaction_20260728.md`.
- Red test: real missing-directory failure and forced `quit` both left the
old buffer unformatted.
- Validation: focused 0/2 → 2/2, then success/failure/encoding/retry 4/4;
complete ERT 102/102; C/Elisp fuzz 300/300; warning-clean byte
compilation; checkdoc.
- Impact: `ekp-region.el`, `tests/ekp-region-tests.el`, user/developer
documentation, audit status, phase records, and a design postmortem.
- Completed: 2026-07-28; `issue002` is resolved.
- task005 [x] Make buffer integrations composable and span/mode-owned.
- Source: `issue003`, audit P1-03/P2-06, and
`plan_integration_lifecycle_20260728.md`.
- Red test: existing filter output was lost and final unjustify left the
EKP filter/hooks installed (0/2).
- Validation: focused 2/2 then five public composition/lifecycle cases;
complete ERT 107/107; C/Elisp fuzz 300/300; warning-clean byte
compilation; checkdoc.
- Impact: `ekp-region.el`, `tests/ekp-region-tests.el`, public/developer
documentation, audit status, phase records, and a design postmortem.
- Completed: 2026-07-28; `issue003` is resolved.
- task006 [x] Eliminate ERT order-dependent false greens.
- Source: `issue004`, audit P1-04, and
`plan_test_isolation_20260728.md`.
- Red test: loading C and running only
`ekp-test-parshape-bypasses-c` failed 0/1.
- Validation: focused public-dispatch/config-restoration/C-parameter
matrix 3/3; reproducibly permuted ERT 108/108; every ERT independently
passed in a fresh Emacs process, 108/108.
- Impact: core fixture and dispatch regression, reusable test runners,
CI, developer documentation, audit status, phase records, and a testing
postmortem.
- Completed: 2026-07-28; `issue004` is resolved.
- task007 [x] Make GUI verification fail closed in automation.
- Source: `issue007`, audit P2-05, and
`plan_gui_verification_20260728.md`.
- Red test: the forced noninteractive failure control had no report
assertion boundary, 0/1.
- Validation: focused forced-failure/success ERT 2/2; default and
permuted full ERT 110/110; real GUI matrix 7/7; inspected clean
fullscreen single-window screenshot.
- Impact: GUI verification result/report contract, batch-safe negative
tests, test loaders, public/developer documentation, audit status,
phase records, and a testing postmortem.
- Completed: 2026-07-28; `issue007` is resolved.
- task008 [x] Close the C API input, error, and arithmetic contract.
- Source: `issue005`, audit P2-02, and
`plan_c_api_contract_20260728.md`.
- Red tests: malformed/short/mismatched/out-of-range/penalty inputs and
extreme valid int32 arithmetic failed 0/6; public dispatcher swallowed
a forced module error, 0/1.
- Validation: focused C boundary 6/6 and public error propagation 1/1;
warning-clean release and sanitizer compilation; full ERT 116/116;
C/Elisp fuzz 300/300.
- Impact: C API validation, 64-bit DP intermediates, module version 1.6,
Elisp backend error propagation, tests, public/developer documentation,
audit status, phase records, and an architecture postmortem.
- Completed: 2026-07-28; P2-02 is resolved. `issue005` remains open until
the build invocation/profile slice is complete.
- task009 [x] Make C builds shell-free, portable by default, and profiled.
- Source: `issue005`, audit P2-03, and
`plan_c_build_boundary_20260728.md`.
- Red tests: capture the interactive build process shape for a module path
containing spaces and reject an unknown profile.
- Validation: focused ERT, real portable/native/debug/sanitize builds,
a copied build from a whitespace path, full ERT/fuzz, byte compilation,
checkdoc, and diff review.
- Impact: `ekp-utils.el`, `ekp_c/Makefile`, CI build arguments, tests,
public/developer/C documentation, audit status, and phase records.
- Completed: 2026-07-28; process/profile red 0/2 → 2/2; all four profiles
compiled warning-free; portable succeeded through the real interactive
command and from a whitespace path. `issue005` is resolved.
- task010 [x] Close repository-local release and CI governance.
- Source: `issue009`, audit P2-07, and
`plan_release_governance_20260728.md`.
- Red test: a static release gate must reject floating action refs, absent
Windows coverage, ignored `.phrase` truth, or inconsistent package/C
version declarations.
- Validation: red/green gate, workflow syntax/static inspection, default
ERT, and `git diff --check`.
- Impact: CI, `.gitignore`, release documentation, audit/phase records, and
a release-governance postmortem.
- Completed: 2026-07-28; invariant gate red → green, pinned package-lint
passed, workflow YAML and shell syntax passed, and default ERT was
119/119. `issue009` is resolved locally; no remote release action was
performed.
- task011 [x] Make dictionary syntax support and provenance auditable.
- Source: `issue006`, audit P2-04, and
`plan_dictionary_governance_20260728.md`.
- Red tests: affected-language golden behavior and a manifest/updater
verifier must fail against the unpinned moving-HEAD bundle.
- Validation: golden red/green, 49/49 checksum/source/license manifest,
two identical pinned update outputs, full ERT, byte compilation,
checkdoc, and diff review.
- Impact: hyphen parser/tests, dictionary updater and metadata, public/
developer/audit documentation, phase records, and an architecture
postmortem.
- Completed: 2026-07-28; affected-language red 0/2 → 2/2; offline and
upstream 49/49; two exports identical; full ERT 121/121; compile,
checkdoc, shell/YAML, and diff checks passed. `issue006` is resolved.
- task012 [x] Make measured tokenizer/insertion growth linear and cache nil.
- Source: `issue008`, audit P3-01, and
`plan_hot_loop_performance_20260728.md`.
- Red tests: two no-break lookups must compute once; long/propertized
tokenization and dense insertion must retain exact output.
- Validation: before/after 1k8k scaling, focused/full ERT, warning-as-error
byte compilation, checkdoc, fuzz, and diff review.
- Impact: tokenizer/hyphen cache/insertion, benchmark/tests, developer/
audit documentation, phase records, and a performance postmortem.
- Completed: 2026-07-28; nil-cache red 0/1 → 1/1; focused output
regressions 3/3; 8,000-character tokenizer/insertion improved from
3.133/0.945 s to 1.100/0.013 s; full ERT 124/124, fuzz 300/300,
warning-as-error production compilation and checkdoc passed.
- task013 [x] Clarify and test interactive protection workflows.
- Source: audit P3-03, ultragoal G009, and
`plan_interactive_protection_20260728.md`.
- Red tests: drive all four no-break/verbatim commands interactively,
assert real formatter behavior and session-lifetime feedback, and
require a standard mode menu exposing the workflows.
- Validation: focused red/green ERT, default/permuted/isolated full ERT,
warning-as-error byte compilation, checkdoc, and diff review.
- Impact: public commands/mode map, region tests, bilingual user
documentation, audit/phase records, and a workflow postmortem.
- Completed: 2026-07-28; focused public-path red 0/3 → 3/3; default
and seeded-permuted ERT 127/127; every ERT passed in a fresh Emacs
process; warning-as-error production compilation, checkdoc, release,
dictionary, and diff gates passed.
- task014 [x] Centralize the remaining duplicated core layout rules.
- Source: audit P3-02 and `plan_core_rule_ownership_20260728.md`.
- Red tests: require a directly testable edge-space exclusion rule and
one complete, nonsticky render-marker vocabulary.
- Validation: focused red/green ERT, full ERT, C/Elisp fuzz,
warning-as-error byte compilation, checkdoc, and diff review.
- Impact: `ekp.el`, `ekp-region.el`, core tests, developer/audit records,
and an architecture postmortem; no public behavior or ABI change.
- Completed: 2026-07-28; direct rules red 0/2 → 2/2; related focused
invariants 4/4; full ERT 129/129; C/Elisp fuzz 300/300;
warning-as-error compilation, checkdoc, release, dictionary, and diff
gates passed.
- task015 [x] Run bounded anti-slop cleanup and the final independent gate.
- Source: ultragoal G010, `ai-slop-cleaner`, `code-review`, and
`plan_final_cleanup_review_20260728.md`.
- Behavior lock: 129/129 ERT and 300/300 fuzz before cleanup.
- Cleanup: delete only proven dead/pass-through accessors; classify every
fallback-like path and retain grounded boundary behavior.
- Validation: all repository quality gates plus independent
`code-reviewer` APPROVE and `architect` CLEAR.
- Impact: internal cleanup, final records, and review artifacts; no
public behavior, baseline, ABI, or remote release action.
- Completed: 2026-07-28; removed nine dead pass-through accessors and the
private substring-filter dependency; classified all fallback-like paths
with no masking fallback retained; default/permuted/isolated ERT
130/130, fuzz 300/300, warning-clean Elisp/C builds, checkdoc,
package-lint, release/dictionary/static gates, and GUI matrix 7/7
passed. Independent review covered 98 paths with zero findings and
returned `APPROVE`; architecture review returned `CLEAR`.
- task016 [x] Make the same-string paragraph fast path property-sensitive.
- Source: `issue010`, independent final code review, and
`plan_property_sensitive_fast_path_20260728.md`.
- Red test: warm one string object, mutate its `ekp-no-break` property,
and require the next lookup to match a fresh paragraph for both CJK and
Latin-with-space inputs.
- Validation: focused red/green, default/permuted/isolated full ERT, fuzz,
static/build/GUI gates, and independent review.
- Impact: paragraph cache identity, core regressions, developer/audit/
phase records, and a cache-ownership postmortem; no public API or C ABI
change.
- Completed: 2026-07-28; same-object property mutation red 0/1 → 1/1;
focused cache matrix 6/6; default and seeded-permuted ERT 130/130;
every one of 130 ERT tests passed in a fresh Emacs process; C/Elisp fuzz
300/300; warning-as-error compilation, checkdoc, package-lint, four C
profiles, release/dictionary/static gates, and GUI matrix 7/7 passed.
Independent review confirmed the implementation fix and architecture
ownership; `issue010` is resolved.
- task017 [x] Rename the editor integration module around its buffer owner.
- Source: user request and `plan_buffer_module_naming_20260728.md`.
- Behavior lock: run the existing 130-test ERT suite before editing; the
rename must preserve rendered output, logical serialization, and every
public region/buffer workflow.
- Change: rename the file/feature/customization/configuration/private/test
namespaces from `ekp-region` to `ekp-buffer`, while keeping public
operation names that correctly describe region or buffer operands.
- Validation: focused/default/permuted/isolated ERT, C/Elisp fuzz,
warning-as-error byte compilation, checkdoc, release/package/static
gates, stale-name scan, and full diff review.
- Impact: editor integration source/tests, automation, bilingual public
and developer documentation, current audit records, phase records,
changelog, and a naming decision postmortem.
- Completed: 2026-07-28; pre-change ERT 130/130; focused buffer ERT 44/44;
default and seeded-permuted ERT 130/130; every one of 130 ERT tests
passed in a fresh Emacs process; C/Elisp fuzz 300/300; warning-as-error
byte compilation, checkdoc, pinned package-lint, release/shell/diff
gates, active stale-name scan, and mechanical equivalence check passed.
- task018 [x] Record the text-property-only layout and hyphen feasibility.
- Source: `issue011`, the user's 2026-07-29 display-property constraints,
and `tech-refer_text_property_layout_20260729.md`.
- Scope: documentation and clean GUI probes only; no runtime source or
test implementation changes.
- Evidence: confirm absolute-pixel `min-width`, real-space
shrink/stretch through `space-width`, `line-prefix` indentation, and a
replacing display string that renders a source grapheme plus
discretionary hyphen and visual newline without changing source
characters.
- Validation: official GNU Emacs documentation/source review; clean
Emacs 30.2 GUI glue, hyphen/newline, vertical-motion, and line-prefix
probes; document link/traceability review; `git diff --check`.
- Impact: `issue011`, one new technical reference, current OMX plan/test
supersession notices, phase/global change records; no runtime behavior.
- Completed: 2026-07-29. Static LTR feasibility is recorded; `issue011`
remains open for the full GUI/editor-semantic gate, live editing, and
the one-buffer/multiple-window decision.
- task019 [x] Extract a semantic KP layout plan and preserve string output.
- Source: `issue011`,
`spec_text_property_layout_engine_20260729.md`, and
`plan_text_property_layout_engine_20260729.md` M1.
- Red tests: require line/source/gap/break/hyphen plan data and require the
existing string renderer to consume that plan without changing its
`equal-including-properties` output.
- Validation: focused red/green ERT, full core ERT, C/Elisp parity, fuzz,
warning-as-error byte compilation, and checkdoc.
- Impact: `ekp.el`, core tests, developer reference, and phase records.
- Completed: 2026-07-29; focused plan tests 0/3 → 3/3; the public string
renderer consumes `ekp-layout-plan`; full ERT passed 133/133, C/Elisp
fuzz passed 300/300, and warning-as-error compilation, checkdoc, and
diff checks were clean.
- task020 [x] Replace physical buffer formatting with text-property projection.
- Source: `issue011`,
`spec_text_property_layout_engine_20260729.md`, and
`plan_text_property_layout_engine_20260729.md` M2.
- Red tests: unchanged source characters/positions/ticks/undo/modified
state; exact cleanup and foreign-property restoration; no overlays or
synthesized layout characters; copy/save/isearch see logical text.
- Validation: focused ERT plus clean Emacs 30.2 GUI glue, CJK break,
Latin discretionary hyphen, indentation, point, region, and mouse probes.
- Impact: `ekp-buffer.el`, buffer/GUI tests, commands, integrations,
bilingual docs, and phase records.
- Completed: 2026-07-29. `ekp-buffer.el` now projects only onto existing
source characters with owned, nonsticky text properties and creates no
overlay. Buffer ERT passed 71/71. A clean Emacs 30.2 GUI probe rendered
exact 164px ASCII and zero-source CJK glue, display-only Latin hyphens
and breaks, `line-prefix`, point/vertical motion/region/mouse behavior,
unchanged source text, and zero overlays. The 7-case scale/remap/fringe/
width matrix passed.
- task021 [x] Implement active-line and bounded-suffix live layout.
- Source: the user's seamless-editing requirement,
`spec_text_property_layout_engine_20260729.md`, and
`plan_text_property_layout_engine_20260729.md` M3.
- Red tests: unfinished line stays natural; overflow pushes a minimal
suffix; deletion pulls back; stable prefix does not move; stale
generations cancel; composition defers; no delayed whole-paragraph snap.
- Validation: deterministic ERT, performance budgets, and dynamic GUI
recordings with temporal review.
- Impact: `ekp-buffer.el`, live-edit state/tests/benchmarks, mode help,
bilingual docs, and phase records.
- Completed: 2026-07-29. Live edits publish synchronously from a stable
active-flow anchor, preserve a natural unfinished line, push the
minimal overflow suffix, pull text back on deletion, converge on an
unchanged line signature, defer IME composition, reject stale
generations, and bound both lookahead and automatic paragraph work.
The type/delete round-trip regression failed with an extra break before
the stable-anchor fix and now restores exact prior break positions.
Buffer ERT passed 71/71. The 31.71s dynamic GUI artifact recorded
before/immediate/settled overflow and deletion checkpoints; source and
projection hashes were stable after redisplay, deletion restored the
original hashes and two breaks, all assertions passed, zero overlays
were present, and temporal review returned PASS.
- task022 [x] Close the text-property layout architecture and quality gates.
- Source: `issue011`,
`spec_text_property_layout_engine_20260729.md`, and
`plan_text_property_layout_engine_20260729.md` M4.
- Work: resolve documentation drift, record the one-width-per-buffer and
display-conflict decisions, remove dead physical-layout adapters, run
anti-slop cleanup, and obtain independent reviews.
- Validation: default/permuted/isolated ERT, fuzz, warning-clean Elisp/C
builds, checkdoc/static/release gates, GUI matrix, dynamic evidence,
full diff review, code-reviewer APPROVE, and architect CLEAR.
- Impact: runtime/tests/docs/postmortem and `issue011` closure.
- Completed: 2026-07-29. Buffer ERT passed 71/71; full default and
seeded-permuted ERT passed 162/162; every one of 162 tests passed in a
fresh Emacs process; C/Elisp fuzz passed 300/300. Warning-as-error
byte compilation, checkdoc, pinned package-lint, release, bundled and
pinned-upstream dictionary, shell, diff, stale-name, no-overlay, and
dead-private-function gates passed. Portable, native, debug, and
sanitizer C profiles built cleanly. Static GUI verification passed
exact 164px ASCII/CJK glue and the seven-case display matrix; dynamic
verification passed the overflow/type/delete round trip with zero
overlays and stable source/projection hashes. Anti-slop cleanup removed
the dead `ekp-buffer--justified-spans` and `ekp--para-glue-shrink`
functions. Independent code review returned `APPROVE` with no findings;
independent architecture review returned `CLEAR`. `issue011` remains
open only for the required user-visible behavior confirmation.
- Completion-audit correction: the final objective named a phase spec,
but the phase initially had none. Added
`spec_text_property_layout_engine_20260729.md` and linked it from the
plan, tasks, issue, and change record before claiming goal completion.
The same audit corrected the dynamic duration to the manifest's
31.707-second `run-end` value and strengthened the existing display
tests to assert the exact nested `space-width`, `min-width`, and
`line-prefix` values. The strengthened focused tests passed 3/3; the
current full suite passed 162/162; warning-as-error test compilation,
checkdoc, release, artifact-cleanliness, and diff gates remained clean.
- task023 [x] Preserve editor selection state and natural active-line edges.
- Source: `issue012`, `issue013`, and
`spec_text_property_layout_engine_20260729.md`.
- Red tests: reprojection must preserve an inactive mark; one
command-loop space/tab at either active-line edge and whitespace
exposed by deletion must carry no EKP replacing display.
- Work: restore the mark marker without changing `mark-active`; prevent
completed-paragraph edge hiding from running on the active live line.
- Validation: focused red/green ERT plus clean GUI width-key and
single-space checkpoints.
- Impact: `ekp-buffer.el`, buffer/GUI tests, user docs, issue/change
records, and the live-layout decision record.
- Completed: `ekp-justify-region` now restores the mark marker and
`mark-active` independently. Live projection never applies static edge
hiding to the active line and clears from the source span beginning
when its first line is active. Focused red 0/3 → green 3/3; clean GUI
width and single-space checkpoints pass.
- task024 [x] Audit the live interaction matrix for adjacent regressions.
- Source: the user's request for detailed seamless-editing review and
`issue_live_editing_interaction_20260729.md`.
- Work: exercise insertion, consecutive whitespace, deletion/backspace,
newline, yank, undo, point, inactive/active mark, visual-break
boundaries, paragraph transitions, resize, and teardown through public
command paths.
- Validation: deterministic matrix ERT, full repository gates, and a
temporally reviewed GUI recording with explicit invariants.
- Impact: tests/verification artifacts and only root-cause runtime fixes
for newly reproduced defects.
- Completed: added public-command coverage for active/inactive regions,
edge whitespace, consecutive spaces/backspace, yank, newline, undo,
and deletion-exposed whitespace; existing tests cover the remaining
flow, resize, composition, transition, and teardown boundaries. Buffer
ERT passes 79/79; default/random/isolated full ERT passes 170/170;
fuzz passes 300/300; compiler, lint, release, dictionary, static, diff,
and clean dynamic GUI gates pass.
- task025 [x] Replace partial live KP with natural progressive editing.
- Source: `issue014`, the user's approved three-state model, and
`spec_text_property_layout_engine_20260729.md`.
- Red tests: an underfull active tail owns no EKP layout property; natural
wrap aligns only completed rows without a replacing break or live
hyphen; editing/backspacing into a committed row restores native
display; hard-paragraph completion permits one complete KP pass.
- Work: delete the stable-anchor/lookahead/convergence/push-pull live
state machine. Keep a natural active tail, project only gaps on rows
already left by native screen wrapping, and run full KP only at hard
paragraph completion or paragraph exit.
- Validation: focused red/green ERT, full repository gates, and
per-keystroke mixed Latin/CJK GUI recording across wrap and deletion.
- Impact: `ekp-buffer.el`, core live-flow API if it becomes unused,
buffer/GUI tests, bilingual docs, spec/plan, issue/change records, and
the live-layout postmortem.
- Completed: removed stable-anchor/lookahead/convergence/push-pull flow
and its unused core signatures. The live path now clears the active
hard paragraph to native display, commits only internal gap widths on
native rows already left by point, keeps the active tail untouched,
and runs complete KP only on hard-paragraph completion or exit. Live
rows cannot publish prefixes, replacing breaks, or discretionary
hyphens.
- task026 [x] Close natural-live-edit verification and documentation.
- Source: `task025`, `issue014`, and the repository completion contract.
- Work: remove obsolete tests/docs/private APIs, update the durable
design record, inspect the full diff, and retain deterministic temporal
evidence.
- Validation: default/random/isolated ERT, fuzz, warning-as-error
compilation, checkdoc, pinned package-lint, release/dictionary/static
gates, clean fullscreen GUI screenshots, and reviewed recording.
- Impact: tests, documentation, phase closure records, and no additional
runtime abstraction.
- Completed: buffer ERT 75/75; default, seeded-permuted, and isolated full
ERT 164/164; fuzz 300/300; compiler, checkdoc, pinned package-lint,
release, dictionary, pinned-source, ownership, no-overlay, stale-name,
and diff gates pass. Seven static GUI variants pass. The retained
14.82-second fullscreen recording
`/private/tmp/ekp-native-live-2dYu2U/recording.mov` proves native wrap,
deletion restoration, and hard-newline completion without flicker or
non-Emacs frames. `issue014` remains open only for required
user-visible confirmation.
- task027 [x] Make native soft wrapping an owned auto-mode precondition.
- Source: `issue015`, the user's narrow split-window screenshot, and
`spec_text_property_layout_engine_20260729.md`.
- Red tests: enabling `ekp-auto-justify-mode` must locally disable both
explicit line truncation and Emacs's default narrow partial-window
truncation; normal disable, activation failure, and major-mode teardown
must restore the exact prior values and local-binding ownership.
- Work: give the minor-mode lifecycle temporary ownership of
`truncate-lines` and `truncate-partial-width-windows`. Do not synthesize
a break, alter source text, or permanently overwrite user settings.
- Validation: focused red/green ERT, split-window GUI typing across the
native wrap boundary, full repository gates, and temporal review.
- Impact: `ekp-buffer.el`, buffer/GUI tests, bilingual docs, spec,
issue/change records, and the native-live postmortem.
- Completed: the mode owns native soft wrapping before its first reflow,
shares one hook lifecycle for activation and cleanup, and uses
`unwind-protect` so a failed activation restores state before
propagating the original error. Focused lifecycle ERT passes 3/3;
buffer ERT 77/77; default, seed-`20260729`, and isolated full ERT
166/166; fuzz 300/300; compiler, checkdoc, pinned package-lint,
release, dictionary, pinned-source, no-overlay, stale-name, conflict,
artifact, and diff gates pass. Retained 44-column split-window
evidence proves native wrap with `hscroll=0`, exact source text, zero
overlays, and zero live replacing breaks. `issue015` remains open only
for required user-visible confirmation.
- task028 [x] Replace native-row live commitment with semantic hard-line
prefix projection.
- Source: `issue016`, the user's multi-row live-editing screenshot, and
`postmortem/20260729-whole-hard-line-live-prefix.md`.
- Red tests: through public editing paths, require the complete current
hard line to be planned by the existing `ekp-layout-plan`, require only
plan lines before the point-containing line to be projected, require
earlier breaks/glue to change together after later edits, and require
plan cache hits/zero writes when the semantic plan is unchanged.
- Work: delete the native-row freezing model from `ekp-buffer` live
editing. Keep the core DP, C ABI, DP schema, and completed-paragraph
plan semantics unchanged. Add buffer-local live plan history,
line-signature diffing, point-movement boundary updates, fail-closed
conflict handling, and exact cleanup without overlays or source layout
characters.
- Validation: focused red/green ERT for self-insert, yank, delete, real
undo/redo, point movement, hard newline, resize, major-mode change, and
projection failure; buffer/full/random/isolated ERT; C/Elisp fuzz;
warning-as-error compilation; checkdoc; package/release/dictionary/
no-overlay/static gates; performance benchmark with latency and cache
counters; clean GUI dynamic recording proving aligned semantic prefix,
natural point line/suffix, exact source text, zero overlays,
`hscroll=0`, and no delayed snap.
- Impact: `ekp-buffer.el`, live-edit tests/benchmarks, GUI verification,
bilingual user/developer docs, spec/plan/tech reference, issue/change
records, and the superseding live-prefix postmortem.
- Completed: 2026-07-29. The implementation consumes one complete
hard-line `ekp-layout-plan`, projects only semantic lines before the
point-containing line, keeps the active line/suffix natural, assigns
break whitespace to the preceding semantic owner, uses a bounded
16-entry live plan LRU, and publishes only the changed signature
suffix. No task028 change was made to core DP semantics, the C ABI,
DP schema, or plan contract.
- Verification: focused font-context invalidation RED 0/1 → GREEN 1/1;
buffer ERT 92/92; default and seed-`20260729` full ERT 181/181;
181/181 isolated-process ERT with the added regression independently
green; C/Elisp fuzz 300/300; warning-as-error compilation, checkdoc,
pinned package-lint, release, dictionary, pinned-source, static
ownership, and diff gates pass. Reviewed temporal GUI evidence at
`/private/tmp/ekp-semantic-live-v4-vFZTkr` and
`/private/tmp/ekp-semantic-split-v3-uPwuOi` returns PASS. The
GC-excluded C-backend append p99 is below one frame; default-GC raw p99
retains a documented collection-pause risk. Independent code review is
APPROVE and architecture review is CLEAR.
- Closure: task028's implementation gate is complete. `issue016` remains
open until the user personally confirms the visible editing
experience.
- task029 [x] Make the semantic live prefix independent of point-only
motion.
- Source: `issue017`, the user's clarification that an already published
layout is not cursor-owned, and
`postmortem/20260729-editing-frontier-not-point.md`.
- Red tests: after a real source edit publishes a multi-line prefix,
moving point backward and forward inside the same hard line must
preserve the exact owned-property projection, editing frontier,
active index, signatures, plan/cache counters, modified tick, undo
state, and source characters. A real edit in an earlier projected line
must still naturalize that line before mutation and relocate the
frontier afterward. Reflow after point motion must map the preserved
frontier into the new plan.
- Work: store the latest real source-edit position in the live state,
derive the active semantic line from it, preserve it across
width/font/layout reflow, and delete point-driven boundary publication
from `post-command-hook`. Keep leaving-hard-line completion unchanged.
- Constraints: do not modify core DP semantics, `ekp.el`, C ABI, DP
schema, or the layout-plan contract; do not add overlays or source
layout characters.
- Validation: focused RED/GREEN ERT; complete buffer/default/random/
isolated ERT; C/Elisp fuzz; compiler/checkdoc/package/release/
dictionary/static gates; benchmark; clean dynamic GUI motion evidence;
full diff review; independent code and architecture review.
- Impact: `ekp-buffer.el`, buffer and GUI tests, benchmark assertions,
bilingual user/developer docs, spec/plan/technical reference,
issue/change records, and the superseding frontier postmortem.
- Completed: 2026-07-29. Live state now owns a source-relative frontier
marker at the latest real edit. Projection derives its active semantic
line from that marker; `after-change` relocates it, reflow preserves
it, and `post-command-hook` no longer republishes inside the active
hard line. Leaving the hard line and hard-newline completion retain the
existing static transition. Core DP semantics, `ekp.el`, C ABI, schema,
and the layout-plan contract are unchanged.
- Verification: the two focused regressions failed before the fix and
passed afterward; independent review ran 10/10 focused tests and the
buffer suite passes 93/93. Default, seed-`20260729`, and isolated full
suites pass 182/182; C/Elisp fuzz passes 300/300. Warning-as-error
compilation, checkdoc, pinned package-lint, release, dictionary,
pinned-source, no-overlay, no-stale-symbol, and diff gates pass.
Point-motion benchmark p99 is 0.033 ms on the C backend and 0.037 ms
in the independent Elisp-backend review, with zero planner/cache calls
in both. Reviewed 26.6-second GUI evidence at
`/private/tmp/ekp-frontier-live-v3-66WYRW` contains 39 manifest lines,
all staged assertions green, no black segment, and a PASS report.
Architecture review is CLEAR; code review reports zero blockers and
isolates the unrelated append latency as `issue018`/`task030`.
- Closure: task029's developer gate is complete. `issue016` and
`issue017` remain open until the user personally confirms the visible
editing experience.
- task030 [ ] Diagnose and optimize unique-state live append planning at
very narrow widths.
- Source: `issue018` and
`postmortem/20260729-narrow-live-append-replanning.md`.
- Problem: after task031, the checked-in 80-pixel workload performs zero
planning on ordinary same-row edits but still records 15 structural
boundary plans across 291 appends. A fresh GC-excluded run measured
C median 2.177 ms/p99 51.170 ms and Elisp median 2.176 ms/p99
187.499 ms; the high percentiles now belong to permitted commit spikes,
not per-key frontier replanning.
- Work: profile a documented width/length and commit-event matrix,
identify the surviving structural-planning owner of the cost, and
implement the smallest exact optimization that reduces commit latency.
- Constraints: preserve KP output semantics, exact cache identity,
task031's stable transaction/structural-commit contract, source-clean
text properties, zero-work point-only motion, and the existing core
DP/C ABI/schema contracts unless a separately approved architecture
decision changes them. Do not use debounce, stale reuse, skipped edits,
timers, or global GC changes to hide mutator latency.
- Validation: result-equivalence ERT/fuzz, profiler before/after evidence,
repeatable Elisp/C default-GC and GC-excluded width-matrix benchmarks,
complete repository gates, and dynamic GUI typing evidence.
- Dependency: `task031` is complete. Optimize only the surviving
structural-commit path; the obsolete per-keystroke whole-hard-line path
no longer exists.
- Implementation checkpoint: layout plans now retain prepared paragraph
and context data. Exact plain-text appends retokenize only from the last
complete-word boundary, rebuild derived vectors from the first dirty box,
resume pure-Elisp 1D DP from the earliest state that can reach the new
tail, and reuse unchanged line records. The buffer reconstructs only its
dirty source island and falls back to the complete planner for every
unsupported context. C signed-int validation now uses one module
extraction instead of three Lisp predicate/comparison calls.
- Architecture decision: keep the C module. The frozen baseline places C
DP at only 2.615/2.655 ms p95/p99, and the candidate at
0.697/0.701 ms. Rust would cross the same Emacs module ABI while leaving
Elisp tokenization, measurement, transactions, and property publication
unchanged.
- Performance checkpoint: four interleaved source-instrumented rounds
reduce 80-pixel C p95/p99 from 114.717/119.201 ms to
25.490/25.785 ms (77.78/78.37%) and Elisp from 588.017/597.093 ms to
43.860/47.578 ms (92.54/92.03%). Exact parity, zero-work ordinary keys,
valid GC exclusion, conflict freedom, and all-width non-regression pass.
- Production checkpoint: with production files byte-compiled, three
repeated public-command runs measure append p99 at 1.1581.326 ms for C
and 1.4291.438 ms for pure Elisp; hard-boundary p99 is
1.2511.363 ms and 1.4571.470 ms respectively.
- Verification checkpoint: normal and random-order ERT pass 199/199;
300-case C/Elisp fuzz, portable warning-clean C build and 9/9 C tests,
warning-as-error production compilation, release checks, exact
append-chain review, and the 11-action/33-checkpoint temporal GUI run
pass.
- Remaining gate: the locked source-instrumented evaluator still exceeds
its absolute 16 ms target for C and Elisp. Keep `task030` open rather
than redefining the evaluator after observing the result.
- task031 [x] Replace the overloaded live frontier with committed projection,
a dirty edit transaction, and atomic structural commits.
- Source: `issue019`, the user's stable-editing correction, and
`postmortem/20260729-stable-live-transaction.md`.
- Red tests: drive installed before/after-change and command hooks to prove
that ordinary same-row edits perform no whole-hard-line planning, a
middle-line edit preserves unaffected projection anchors, deleting and
reinserting the same space restores the exact
`equal-including-properties` projection, point-only motion across or
outside the active hard line performs zero planning/property writes, and
the next real edit elsewhere commits the prior dirty transaction.
- Work: delete `frontier = natural suffix` ownership. Store the committed
source, plan, line signatures, spans, and owned projection baseline in
the edit transaction. Naturalize only the dirty edit island, preserve
unaffected break anchors, let native soft wrapping absorb local
push/pull, and replan atomically only when input crosses a visual-row
boundary or another structural commit event occurs.
- Commit events: native soft-wrap crossing, hard newline/paragraph end, the
next real source edit outside the dirty island, explicit paragraph
refill, and width/font/layout-context change. Cursor motion is never a
commit event.
- Constraints: keep core DP, `ekp.el`, C ABI, DP schema, and layout-plan
semantics unchanged; never add overlays or source layout characters;
preserve exact source/editor state and fail closed on conflicts.
- Validation: focused RED/GREEN ERT, complete buffer/default/random/
isolated ERT, C/Elisp fuzz, warning-as-error compile/checkdoc/package/
release/static gates, a re-profiled live benchmark, and temporal GUI
evidence for stable normal input, middle-line edits, reversible edits,
local word migration, hard completion, and point-only motion.
- Impact: `ekp-buffer.el`, buffer/live benchmark/GUI tests, bilingual
user/developer docs, spec/plan/technical reference, issue/change records,
changelog, and the superseding postmortem.
- Implementation: `ekp-buffer--live-state` owns the committed
source/key/plan/signatures/prefix/spans; `ekp-buffer--live-edit` snapshots
that state plus marker offsets and one dirty island.
`ekp-buffer--start-live-edit` naturalizes only the affected span range,
`ekp-buffer--ordinary-live-edit-finished` restores an exact baseline or
publishes only on a real native-row crossing, and hard/elsewhere/
refill/context events commit through their existing boundaries. Lazy
reflow excludes the active paragraph so static chunks cannot detach
live-owned spans. No task031 change was made to `ekp.el` or the core DP.
- Verification: focused RED tests first failed for same-row planning,
middle-anchor invalidation, exact reversal, point-motion work, backward
row crossing, and lazy-reflow ownership, then passed after the owning
fixes. Final runs pass buffer 99/99 and default, seed-`20260729`, and
isolated suites 188/188 each; C/Elisp fuzz passes 300/300. Production
and task test files compile with warnings as errors; checkdoc is empty;
pinned package-lint exits 0; release and 49-entry dictionary-manifest
gates pass. The exact pinned-source gate passed earlier in this task
with unchanged dictionary bytes; two redundant final downloads ended in
GitHub transport `early EOF`, not a content mismatch.
- Performance: at 80 px with GC excluded, same-row cache-revisit work
performs zero plans (C p99 1.627 ms; Elisp p99 1.502 ms), point motion
performs zero plan/cache calls (C p99 0.017 ms; Elisp p99 0.015 ms), and
291 appends contain only 15 permitted structural plans. The remaining
commit spikes are the re-profiled `task030` surface.
- Dynamic evidence: the reviewed 48.95-second fullscreen run at
`/private/tmp/ekp-stable-transaction-final5-2BFryc` contains 55 ordered
checkpoints (11 actions × 5 phases), 17 green assertions at every
checkpoint, one 1434×900 target window, a completed run-end, no black
segment, and a PASS report. It covers middle-row locality, exact
reversal, forward structural crossing, backward deletion stability,
point motion, public yank/undo, resize/context commits, and hard
paragraph completion.
- Closure: developer verification is complete. `issue019` remains open
only for the required user-visible confirmation; `task030` is unblocked.
- task032 [x] Reduce C-backed resize/reflow latency with exact layout parity.
- Source: `issue020` and the user's observed 6070 ms C resize latency.
- Baseline first: freeze the current portable C module and build a
same-machine evaluator that separates complete reflow, plan construction,
Emacs/C marshalling, C DP, and projection publication. Exclude startup,
resize debounce, and GC pauses from the mutator measurement.
- Performance gate: an interleaved baseline/candidate width-and-length
matrix must improve both p50 and p95 by at least 20%, keep candidate p95
at or below 50 ms, and report raw samples rather than a best-of run.
- Correctness gate: every candidate C result must match the frozen C
baseline and Elisp layout exactly; complete ERT, C/Elisp fuzz,
warning-as-error builds, static/release checks, and temporal GUI resize
evidence remain mandatory.
- Constraints: do not weaken cache identity, KP output semantics, the
stable transaction model, or source-clean projection. Do not hide work
with longer debounce, skipped widths, stale results, approximate reuse,
background publication, or global GC changes.
- Stop condition: optimize only the layer proven dominant by the baseline
profile. If C DP is not the owner of most resize time, fix the actual
Elisp/C or projection boundary instead of micro-optimizing the DP.
- Impact: evaluator/benchmark, the proven runtime owner, C module tests,
performance documentation, issue/change records, and a decision
postmortem when the selected optimization is non-obvious.
- Implementation: resolve one paragraph/DP payload per plan, cache natural
gap geometry, omit true no-op gaps, consolidate owned property writes,
and avoid static-plus-live double publication of the active paragraph.
- Result: four interleaved rounds reduce core p50/p95 from
22.949/42.006 ms to 15.318/27.687 ms and complete resize p50/p95 from
28.149/46.611 ms to 15.900/27.487 ms. Exact frozen-C/Elisp parity holds.
- Verification: normal and permuted 193-test suites, the per-test isolated
runner, 300-case fuzzing, warning-as-error compilation, package-lint,
checkdoc, release checks, and reviewed dynamic GUI evidence pass.
- Closure: developer verification is complete. `issue020` remains open
only for the required user-visible confirmation.
- task033 [x] Prevent rigid inline atoms from fragmenting preceding CJK.
- Source: `issue021`, the user's 2026-08-01 showcase screenshot, and
`plan_text_property_layout_engine_20260729.md` M12.
- Red tests: reproduce the deterministic batch underfull-to-overfull jump
and the real 280px showcase path; require the fullest permitted prefix
line, one intact atom line, and matching Elisp/C plans.
- Work: correct the emergency transition owner in core DP and its C/loose
equivalents without changing strict K-P results or buffer projection.
- Validation: focused RED/GREEN core and buffer ERT, normal/random/
isolated full ERT, C/Elisp fuzz, warning-as-error compilation, checkdoc,
release/static gates, and inspected static/dynamic GUI evidence.
- Impact: core line breaking, C parity, regression tests, user/developer
documentation, issue/change records, and no new public API.
- Result: retain the original emergency transition and add a fallback only
for a forbidden-run underfull-to-overfull jump. This was later superseded
by the accepted `task036` final-pass model; strict K-P and buffer
projection remain unchanged.
- Verification: the RED boundary was 1 instead of 11. Normal,
seeded-random, and isolated ERT pass 201/201; property fuzz passes
300/300; Elisp/static/release/dictionary and three C profile builds pass.
Reviewed 62.7-second GUI evidence at
`/private/tmp/ekp-atom-gui-final-Beg8hb` passes all nine checkpoints with
an intact atom, one-line full prefix, exact source, zero overlays, active
C engine, and no black or transient vertical frame.
- Closure: developer verification is complete. `issue021` remains open
only for the required user-visible confirmation.
- task034 [x] Make break policy configurable and eliminate inline-code
induced orphan glyph lines.
- Source: the approved RALPLAN PRD
`.omx/plans/prd-ekp-configurable-break-policies.md`, test spec
`.omx/plans/test-spec-ekp-configurable-break-policies.md`, and the
user's 2026-08-01 follow-up screenshot showing the showcase no longer
collapses into the previous one-glyph cascade but still permits
pathological single-CJK source lines around the inline atom, including
`行`, `内`, and `永`.
- Work: implement the locked A2/B2/C2/D1/E1/F1/G2/H1/I1/J1/K1/M1 policy
contract. Inline code defaults to legal wrapping with no dictionary
hyphenation; block code remains verbatim; explicit `ekp-no-break` stays
rigid forever; automatic no-break downgrades to no-hyphen when wider than
the effective measure; URL/path/identifier default to no-hyphen; compact
number-unit tokens default to no-break; kinsoku defaults to `common`;
ordinary overlong tokens default to current emergency output; and buffer
measure defaults to the narrowest live window with fixed and max-cap
alternatives.
- Public configuration: add global, major-mode profile, buffer/file/dir
local, and region controls for inline code, hyphenation, token policies,
kinsoku profiles, overlong-token behavior, and buffer measure. Region
`ekp-break-policy` provides `normal`, `hyphenate`, and `no-hyphen`; the
existing `ekp-no-break` remains the only manual hard-atom owner.
- Red tests: prove inline faces no longer cause paragraph skipping; prove
region > explicit local > mode profile > global precedence; prove token
classification/downgrade behavior; prove `common`/`zh`/`ja`/`off`/custom
kinsoku; prove `emergency`/`overflow`/`natural` overlong modes; prove
`narrowest-window`, fixed, and `(max . PIXELS)` measure resolution; and
reproduce the current showcase orphan-glyph screenshot as a failing
public path before the fix, with an oracle that rejects any pathological
single-CJK source line in the crafted paragraph when a legal
non-emergency alternative exists.
- Verification: focused RED/GREEN ERT for core, buffer, command, cache, and
diagnostics; C/Elisp parity without changing the 15-argument C entry or
15-field batch payload; 300-case property fuzz; warning-as-error Elisp
compilation; C builds/tests; checkdoc/package/static/release gates; and
clean fullscreen single-window GUI evidence for inline wrapping,
no-orphan showcase text, explicit no-break, block verbatim, measure
modes, and overlong-token modes.
- Impact: `ekp.el`, `ekp-buffer.el`, `ekp_c/ekp_kp.c` only if parity logic
requires a mirrored DP adjustment, tests, bilingual user/developer
documentation, spec/plan/tech reference, issue/change records, changelog,
and a postmortem explaining the policy boundary.
- Implementation evidence recorded: G002 completed core policy compilation
without changing the 15-argument/15-field C boundary; G003 completed
buffer ownership, consult-only mode profiles, local/region controls,
diagnostics, and generation reflow; G004 completed the showcase split
between automatic inline code, explicit no-break, and verbatim block;
G005 performance work removed unconditional policy measure from paragraph
semantic identity, canonicalized adjacent equal filtered property
intervals, moved repeated policy full analysis behind a bounded two-tier
cache, and added a bounded per-paragraph semantic plan cache; G006
cleanup touched only `ekp.el` and `ekp-buffer.el`, removing redundant
policy/cache code while preserving behavior; G007 resolved the first G006
review blockers by returning consumer-owned copies of all plan-owned
mutable payloads from semantic-plan cache hits, keeping `para`
intentionally shared for append identity, and by making malformed non-nil
C single/batch results signal `ekp-backend-contract-error` while
preserving nil-result Elisp fallback and the 15-argument/15-field C
boundary; G008 resolved the final string-leaf context alias by using the
recursive context copier for cons/vector/string payloads both when the
context snapshot/cache key is created and when a plan is returned.
- GUI evidence recorded: `/tmp/ekp-g004-evidence.Tp77dW` has 12/12
checkpoints, a 25.75s/206-frame recording, no black segments, no failed
assertions, automatic inline wrapping 3→2→3 lines across 280→340→280,
every internal inline split as source whitespace, C active, exact source,
zero overlays, and policy no-hyphen→normal settling with generation_delta
1, live nil-plan span 1, stale nil-plan span 0, and current projection.
- Verified so far: G002 focused core policy ERT 22/22, no-break 5/5, cache
15/15, C parity 5/5, 300-case fuzz, warning-clean C build, byte
compilation, checkdoc, and diff-check; G003 core ERT 118/118, buffer ERT
119/119, watcher invariants 2/2, byte compilation, checkdoc, and
diff-check; G004 GUI verifier ERT 7/7 and clean GUI evidence; G005
formal four-interleaved evaluator layout parity true, core baseline
p50/p95 38.6679/51.6782ms versus candidate 21.7102/33.0040ms for
43.8549%/36.1354% gains, resize baseline p50/p95 43.3831/55.6250ms
versus candidate 22.0919/32.8202ms for 49.0773%/40.9973% gains, both
candidate p95 values under 50ms and both gain sets at least 20%; live
evaluator source-instrumented locked goal remains the known
`validation_failed` debt, with parity, zero-work, GC, conflict, and
all-width-nonregression true, current C p95/p99 26.449/26.740ms
(76.65%/77.23% improvement), current Elisp p95/p99 49.940/52.017ms
(91.64%/91.35% improvement), consistent with historical open `issue018`
and not a regression; byte-compiled production public path passed three
runs with zero GC, C append p99 1.361-1.368ms, C hard p99
1.876-1.891ms, Elisp append p99 1.692-1.775ms, and Elisp hard p99
2.100-2.230ms; default ERT 262/262, seeded permuted ERT seed 20260728
255/255, isolated per-test process suite exit 0, targeted alias guard
passed, property fuzz 300/300, warning-as-error byte compilation,
package-lint pinned at `35996f478d81e51dae4fa30d051f741895d07399` exit
0 with only an external obsolete warning from the local names dependency,
empty checkdoc, release, 49-entry dictionary manifest, pinned dictionary
update, shell syntax, CI YAML, diff-check, portable/native/debug/sanitize
warning-clean C builds, module 1.6/4-thread smoke, and focused C 19/19
including the 15-argument/15-field boundary passed; G006 targeted
cleanup verification passed 6/6 and 3/3, full core ERT 125/125 and
buffer ERT 120/120 passed, and byte compilation, diff-check, and C build
gates passed; G008 formal resize parity true, core baseline/candidate
p50/p95 36.595/48.357ms versus 23.266/36.363ms for 36.42%/24.80%
gains, resize baseline/candidate p50/p95 41.443/53.541ms versus
23.761/36.889ms for 42.67%/31.10% gains, ERT 268/268, fuzz 300/300,
release gates pass, and byte-compiled current public path records zero
GC with C append/hard p99 1.440-1.464/2.022-2.050ms and Elisp
append/hard p99 1.648-1.687/2.136-2.335ms, all below 16ms.
- Closure: task034 developer implementation, repository gates, performance
gates, cleanup gates, G007 first-blocker fixes, and the G008 final
string-leaf context fix are complete.
`issue021` is closed after user visual confirmation, historical `issue018`
remains open, and final independent review returned APPROVE with
architecture status CLEAR.
- task035 [x] Falsify line-width-scaled final-pass emergency stretch.
- Source: `issue021`, the user's 2026-08-02 narrow-showcase screenshot,
and the G009 upstream K-P review.
- Red tests: reproduce the mixed showcase paragraph at narrow widths and
reject any isolated CJK source line through the core, public buffer path,
and GUI oracle.
- Work attempted: keep the strict pass unchanged; in the final pass add a
line-width-sized finite background emergency stretch to ordinary
underfull candidates and score them through adjustment ratio, badness,
fitness, and demerits. Keep the fixed artificial emergency transition
only for truly overfull first permitted hard/atomic runs. Do not add
CJK-orphan, unit, or screenshot-specific penalties.
- Validation: focused core single-CJK ERT passes 2/2; focused core, C,
hard-atom, buffer, and GUI-oracle G009 coverage passes 18/18 in the
implementation lane; warning-as-error byte compilation and the C build
pass. Full regression then falsified the line-width stretch detail: it
made emergency stretch scale with the candidate measure instead of a TeX
fixed dimension and regressed existing emergency behavior.
- Impact: `ekp.el`, `ekp_c/ekp_kp.c`, tests, bilingual documentation,
phase records, changelog, and postmortem. Public configuration and the C
ABI are unchanged.
- Closure: task035 is closed as a disproved implementation attempt, not as
the accepted current behavior. Continue with `task036`.
- task036 [x] Implement fixed-dimension final-pass emergency stretch and
preserve the final active path.
- Source: task035 full-regression failure, `issue021`, and the G009
original K-P correction.
- Work: replace the candidate-width/line-width emergency stretch detail
with a TeX-style fixed dimension exposed as
`ekp-emergency-stretch-pixel`. A nil value auto-resolves to roughly three
display-font `M` widths; a non-negative integer fixes the stretch in
pixels. Keep the strict pass unchanged and the 15-field C paragraph ABI
stable. In the final pass, preserve the best provisional overfull path
with tight fitness and zero incremental demerits only when no normal
active path survives to that breakpoint. Do not add CJK-orphan, unit,
atom-adjacency, or screenshot-specific penalties.
- Red tests: add five focused RED cases covering fixed dimension versus
measure scaling, nil auto resolution, non-negative integer override,
Elisp/C parity without C ABI growth, and renderer width/glue consistency.
- Regression gate: the original three regressions that task035 broke must
stay green alongside the new RED cases.
- Full gate: focused G009 core/public-buffer/C parity and GUI-oracle
coverage, final fullscreen dynamic GUI evidence, default/permuted/
isolated ERT, 300-case fuzz, warning-as-error byte compilation, C builds,
checkdoc/package/static/release gates, and diff-check.
- Result: production Elisp 1D, looseness/parshape, and C implementations
match; renderer glue distribution uses the chosen line's actual rest;
explicit hard atoms forbid only interior breaks and may share an overflow
line with preceding ordinary content. The test oracle now requires a
nonempty source-covering plan so an empty result cannot false-green.
- Validation: focused root regressions pass 8/8, emergency selection passes
10/10, core/buffer/GUI oracle passes 8/8, full ERT passes 288/288, seeded
and isolated suites pass 279/279, fuzz passes 300/300, portable/native/
debug/sanitize C builds and warning-as-error production plus changed-test
byte compilation pass, checkdoc/package/static/release gates pass, formal
core/resize performance gates pass, and reviewed 42.78-second
single-window dynamic GUI evidence at
`/tmp/ekp-g009-evidence-retry.UOpPNp` returns PASS.
Final independent code review returns APPROVE and architecture review
returns CLEAR.
- Closure: developer implementation and full quality gate complete;
`issue021` closed after the user's 2026-08-02 visual confirmation.

View File

@ -1,408 +0,0 @@
# Technical Reference: Text-Property-Only KP Layout 2026-07-29
## Status and Reader Task
Implemented by `task019` through `task022` for static/source-clean
projection, then refined by later live-editing tasks. `task028` supersedes
the native-row live planning unit with semantic hard-line prefix
projection. This record is the technical reference for the semantic layout
plan, text-property-only buffer projection, live editing boundary, and
their explicit constraints.
## User Constraints
- Do not use overlays.
- Do not insert glue spaces, soft newlines, or discretionary hyphens into
buffer text.
- Express layout through text properties on existing source characters.
- Keep the source character sequence and all logical buffer positions
unchanged.
- An unfinished active line and later live-edit behavior remain separate
product concerns; a static display proof does not establish seamless
editing.
## Conclusion
Emacs 29.1 and later expose enough display primitives to make a
text-property-only KP renderer plausible:
- `(min-width ((PIXELS)))` gives a source span an absolute minimum display
width and pads only in the display layer.
- `(space-width FACTOR)` scales existing ASCII space glyphs without
replacing their source characters.
- Combining `space-width` with `min-width` expresses both shrink and stretch
while using `min-width` as an exact pixel floor after floating-point
rounding.
- `line-prefix` supplies indentation for non-continuation display lines.
- A replacing `display` string attached to an existing grapheme can show
that grapheme followed by a discretionary hyphen and a display-only
newline. The original grapheme remains in the buffer.
These primitives close the representation gap for the implemented
single-authoritative-width buffer renderer. Foreign ownership, multiple
windows, and seamless incremental editing are governed by the decisions
below rather than inferred from the primitives alone.
The package baseline does not need to move: Emacs `NEWS.29` records
`min-width` as a new `display` property, and the repository's CI exercises
the declared Emacs 29.1 floor.
## Confirmed Emacs Semantics
### Absolute pixel minimum width
The `min-width` display specification has the form
`(min-width (WIDTH))`. The outer one-element list identifies a contiguous
display run by `eq`. A pixel expression of the form `(N)` means exactly
`N` pixels, so the fully nested form for 20 pixels is:
```elisp
(min-width ((20)))
```
Emacs displays the underlying text normally and appends display-only white
space if the rendered span is narrower than the requested minimum.
Each independently padded adjacent span must receive a distinct identity
list. Reusing the same inner list object can make Emacs treat adjacent
spans as one run and add padding only at the end of the combined run.
References:
- [Other Display Specifications](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
- [Pixel Specification for Spaces](https://www.gnu.org/software/emacs/manual/html_node/elisp/Pixel-Specification.html)
- [Emacs 30.2 display implementation](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L5606-L5686)
### Scaling real spaces
`(space-width FACTOR)` multiplies the rendered width of every ASCII space
covered by the property. A factor below 1 shrinks; a factor above 1
stretches.
The Emacs 30.2 implementation adds three boundaries beyond the manual's
summary:
- `FACTOR` must be a number greater than zero.
- The graphical display path checks specifically for ASCII `SPC`.
- The specification is ignored on a non-window-system frame.
It does not affect tabs. NBSP, ideographic spaces, tabs, and other Unicode
space characters therefore need their own policy; they must not be assumed
to behave like an ASCII word space.
References:
- [Other Display Specifications](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
- [Emacs 30.2 `space-width` parsing](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L6039-L6052)
- [Emacs 30.2 ASCII-space scaling](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L32723-L32729)
### Combining the two specifications
For a real ASCII whitespace run whose measured natural width is `N` and
whose KP target width is `G > 0`, use the conceptual display value:
```elisp
((space-width FACTOR)
(min-width ((G))))
```
where `FACTOR = G / N`.
`space-width` first makes shrink possible. The display engine rounds the
scaled glyph width to pixels; `min-width` then pads any downward rounding
error to the exact target. For a target at least as wide as the natural
run, `min-width` alone is sufficient, but one combined representation can
keep the renderer uniform.
A zero-width source-space run is not represented by `(space-width 0)`,
because zero is rejected. Line-edge spaces must instead be consumed by the
chosen break representation, retained naturally, or handled by a separately
verified display rule.
### Indentation
The `line-prefix` text property adds a display-only prefix to every
non-continuation line. It must cover the complete affected text range so
redisplay can find it at each line start.
If EKP relies on Emacs's native wrapping, continuation lines use
`wrap-prefix`, not `line-prefix`. If EKP realizes each KP break as a newline
inside a replacing display string, the following row is a non-continuation
display line and `line-prefix` is applicable.
Reference:
- [Special Properties: `line-prefix`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html)
## Mapping KP Output to Existing Source Text
| KP result | Text-property-only projection |
| --- | --- |
| Ordinary box | Display the original source grapheme or word unchanged. |
| Latin word glue over ASCII spaces | Apply `space-width`; add `min-width` as the exact pixel floor. |
| CJK or mixed glue with no source space | Apply `min-width` to the preceding source box with target `natural-box-width + glue-width`. |
| Leading indentation | Apply `line-prefix`; use `wrap-prefix` only if the selected break backend creates continuation lines. |
| Break at an existing single space | Replace that space on display with a newline while leaving the source space in the buffer. |
| CJK break with no boundary character | Make the preceding grapheme display as `GRAPHEME + NEWLINE`. |
| Latin discretionary hyphen break | Make the preceding grapheme display as `GRAPHEME + HYPHEN + NEWLINE`. |
| Final line | Remove forced-break/hyphen properties and leave it ragged unless alignment requires a leading prefix. |
The current algorithm already computes exact per-gap pixel results for
stretch and shrink. The planned change is a renderer substitution, not a
new KP cost or distribution model.
## Discretionary Latin Hyphen Without Source Pollution
### Proposed break owner
For a KP break between source positions `P` and `P+1` inside a Latin word:
1. Identify the complete grapheme immediately before the break. Never split
a combining sequence.
2. Leave every source character untouched.
3. Attach a replacing `display` text property to that grapheme.
4. Build the replacement string from the same visible grapheme, followed by
the configured hyphen glyph and `"\n"`.
5. Copy the grapheme's relevant face/font properties to the replacement
string, but remove recursive `display` state.
6. Put a `cursor` property on the reproduced grapheme so point on the
covered source positions is drawn on the grapheme rather than on the
synthetic hyphen or newline.
7. Include the hyphen width in the KP line measurement exactly as the
current renderer already does.
8. Remove or move the property atomically when a later plan chooses a
different break.
Conceptually, breaking `abcdefgh` after `d` displays:
```text
abcd-
efgh
```
while the source characters remain exactly:
```text
abcdefgh
```
The hyphen and newline exist only inside the `display` property's
replacement string. `char-after`, character positions, regexp search, and
`buffer-substring-no-properties` continue to operate on the original word.
### Why the preceding grapheme owns the replacement
Attaching `"-\nE"` to the next grapheme would make point on that source
grapheme default to the hyphen or newline portion. Attaching
`"D-\n"` to the preceding grapheme keeps the visible source glyph at the
property's logical anchor, and the next source grapheme begins naturally on
the following display line.
### Runtime evidence on Emacs 30.2
A clean GUI Emacs 30.2 probe applied a `display` text property containing
`"d-\n"` with `cursor 1` to the source `d` in `abcdefgh`.
Observed result:
- `substring-no-properties (buffer-string)` remained `"abcdefgh"`.
- `buffer-size` remained 8.
- `count-screen-lines` reported 2.
- `vertical-motion` by one display line moved point from position 1 to
position 5, the source `e`.
- A 20px `line-prefix` placed both visual rows at x=20.
This proves the minimal LTR shape is expressible. It does not yet prove
mouse selection, active-region highlighting, isearch faces, bidi text,
multi-codepoint graphemes, IME composition, or foreign display-property
composition.
References:
- [Display strings](https://www.gnu.org/software/emacs/manual/html_node/elisp/Other-Display-Specs.html)
- [Cursor placement in display strings](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html)
- [Emacs 30.2 cursor handling around display-string newlines](https://github.com/emacs-mirror/emacs/blob/emacs-30.2/src/xdisp.c#L1899-L1956)
## Runtime Evidence for Exact Glue
A clean GUI Emacs 30.2 probe measured the current font's ASCII space at
7px, then applied the combined `space-width` and `min-width` display value
to two real source spaces:
- target 3px rendered as exactly 3px;
- target 20px rendered as exactly 20px;
- the buffer still returned the unchanged source characters `"a b"`.
This proves one-font, one-window shrink and stretch. The production gate
must repeat the probe across text scaling, face remapping, font fallback,
mixed property runs, and window resizing.
## Text API Boundary
This direction removes character pollution but not all property metadata.
- `buffer-substring-no-properties`, direct character access, search,
syntax parsing, markers, point, and character counts see the original
source characters.
- `buffer-string` and `buffer-substring` copy text properties into the
returned string. Lisp that explicitly examines properties can observe
EKP's display metadata.
- Property-only updates must use `with-silent-modifications` so layout
refresh does not alter the modified flag, ordinary undo history, or
external change hooks.
- EKP-owned properties must be nonsticky so new user input does not inherit
a stale break, hyphen, width, or prefix.
- Copy/yank and insertion of propertized strings require an ownership rule
that strips or regenerates EKP layout properties rather than carrying a
stale layout into another location.
References:
- [Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Text-Properties.html)
- [Examining Buffer Contents](https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html)
- [Changing Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Changing-Properties.html)
## Implementation Decisions and Remaining Boundaries
### Chosen break semantics
The replacement-string approach forces the exact KP break and makes
`line-prefix` usable. A native-wrap alternative might avoid replacing the
break-owner grapheme by filling the line exactly with `min-width`, but it
still needs proof that Emacs never wraps at an earlier legal boundary and
that exact-edge stretch glyphs remain on the intended line.
The implementation uses the replacing-string backend because it represents
the chosen KP break explicitly. Native wrapping is not a fallback.
### Foreign `display` ownership
Text has one effective `display` property value at each position. Parallel
non-replacing specifications can coexist in one value, but a foreign
replacing string, image, invisibility rule, composition, or font-lock
manager can conflict with EKP's width or break owner.
EKP rejects the affected hard paragraph with an explicit diagnostic. It
never erases, merges, or silently reorders a foreign replacing
specification.
### Graphemes, bidi, and shaping
A replacing display string is treated as one display object for
bidirectional reordering. The first implementation slice should therefore
target LTR Latin hyphenation and CJK. Arabic, Hebrew, Indic shaping,
ligatures, and multi-codepoint graphemes remain gated until their cursor,
selection, and shaping behavior is proven.
### Multiple windows
Text properties belong to the buffer, not a window. One property set cannot
simultaneously encode two different KP break plans for the same buffer shown
at different widths.
The narrowest live window showing the buffer is authoritative. Wider
windows can retain right-side space but cannot overflow. Independent
simultaneous width plans are explicitly outside this text-property-only
architecture.
### Live editing
The current target policy separates stable editing from global KP
commitment. One committed projection owns the hard-line baseline source,
semantic plan, line signatures, projected spans, and break anchors. One
dirty edit transaction snapshots that baseline and naturalizes only the
affected local island.
Ordinary input inside the same native visual row performs no whole-hard-line
planning. Existing glue and native soft wrapping absorb local changes while
unaffected anchors remain installed. If the row overflows, native wrapping
moves only the required local words. Crossing into a new native visual row
is a structural commit: `ekp-buffer` calls or reuses the existing
whole-hard-line `ekp-layout-plan` once, then old/new semantic signatures
limit one silent prefix publication.
Hard newline/paragraph completion, the next real edit outside the dirty
island, explicit refill, and width/font/layout-context changes are the other
commit events. Point-only motion is not a layout event anywhere. A reversible
edit restores the saved owned projection directly and
`equal-including-properties`.
This remains a buffer-layer policy, not a DP variant. Core DP plans complete
text and receives no point, marker, window, buffer, redisplay, transaction,
or composition state. The C ABI, DP schema, and plan contract do not change.
Static paragraph-edge hiding applies only to committed projected lines. The
dirty edit island retains leading/trailing source whitespace with no
replacing display, so a single space or tab is visible in the same
command-loop turn. Reprojection restores the mark marker and `mark-active`
as separate editor state; `set-mark` is not a valid positional restoration
API because it activates an otherwise inactive mark.
Automatic planning is bounded by
`ekp-auto-justify-paragraph-limit`. An oversized single hard line stays
natural and diagnostic rather than blocking input; the explicit
`ekp-refill-paragraph` command bypasses this limit.
## Rejected Shortcuts
- Insert U+00AD, `-`, whitespace, or newline characters: violates the
logical-text requirement.
- Overlay `before-string` or `after-string`: explicitly excluded by the
user.
- Use `min-width` alone for shrink: it only adds padding.
- Use `space-width` alone as an exact-width guarantee: floating-point
scaling is rounded to pixels.
- Treat tabs or all Unicode whitespace as ASCII spaces: contradicted by the
documented and source-level behavior.
- Claim the GUI probe proves seamless editing: it covers a static LTR
display case only.
## Runtime Verification Gate
1. Exact glue:
- 1px through 64px targets;
- shrink and stretch;
- CJK, ASCII space, mixed gap, and repeated spaces;
- text scaling, face remap, and fallback fonts.
2. Breaks and hyphens:
- Latin word break with hyphen;
- CJK break without whitespace;
- break at one and multiple source spaces;
- final-line removal and breakpoint movement.
3. Editor semantics:
- point at every source position;
- `C-n`/`C-p`, visual beginning/end of line, mouse hit testing;
- active region, isearch, kill/yank, undo/redo, and narrowing.
4. Ownership:
- font-lock refontification;
- a foreign replacing `display` property;
- composition and IME preedit;
- exact restoration of pre-existing properties.
5. State invariants:
- unchanged source characters and `buffer-chars-modified-tick`;
- unchanged modified flag, undo list, and external change-hook count;
- no stale property inheritance after insert, delete, split, or join.
6. Architecture decision:
- explicitly resolve or relax simultaneous different-width windows
before calling the renderer complete.
## Stop Condition
This research task is complete when the confirmed primitives, proposed
hyphen owner, known limitations, and runtime evidence are recorded.
The original static feasibility and one-buffer/multiple-window gates were
satisfied by `task019``task022`.
`task028` proved whole-hard-line plan consumption and semantic signature
publication. `task029` proved that point-only motion must perform zero work.
`issue019`/`task031` supersede their per-edit trigger ownership: a frontier
cannot simultaneously be latest edit, natural suffix, and projection reuse
boundary. Whole-hard-line planning remains valid at structural commits, not
after every key.
The task029 performance audit recorded 291 unique append plans and zero cache
hits at 80 pixels. `task031` now removes ordinary same-row planning: the same
291-edit workload records 15 structural plans, while same-row revisit and
point-motion scenarios record zero plans. The remaining C/Elisp p99 spikes
coincide with permitted commit events; `issue018`/`task030` now own that
unblocked, narrower performance surface.

View File

@ -1,82 +0,0 @@
# P1+P2 功能阶段笔记(2026-07-26 起)
> 用户指令:高质量完成 P1(标点挤压、ragged 模式、no-break API)+ P2(悬挂、
> parshape/首行缩进、连续标点);代码块等特殊文本需正确处理;**通用机制优先,
> 万不得已才做场景特化**。
## 总体架构决策
1. **地基 = 逐间隙断行许可(breaks-allowed)+ 标点独立成盒**
禁则从"吞噬式附着"迁移为 DP 层的断点禁止;所有后续特性(NBSP、
no-break 区间、行内 verbatim 原子、标点类别)都是这套机制的实例。
2. **Emacs 显示引擎约束(已确证)**:无法缩减字形 advance(无负宽
display)→ CLREQ 行中标点挤压不可渲染;行首/行尾挤压视觉上等价于
"悬挂"(protrusion)→ P1-1 + P2-4 + P2-6 统一为**边缘突出机制**,
按字符类配比率(可扩展到拉丁连字符突出 = microtype)。
3. ragged-right/left/center:DP 侧 = 刚性 glue(stretch/shrink 数组置零)
+ 每行额外伸展量 R(badness 以 R 为 flexibility);渲染侧分派剩余量
(右/左/对半)。C 只需 +1 标量。
4. parshape/首行缩进:每行宽依赖行号 → 复用 looseness 的 2D DP,
elisp-only(C 自动旁路,同 looseness 先例)。
5. verbatim:段落级豁免(region 层谓词/属性)+ 行内原子
(ekp-no-break 属性 → 禁断点 + 刚性 glue + 禁断词)。
## 关键实现事实(读码结论)
- tokenizer 附着逻辑在 ekp-utils.el `ekp--handle-cjk-char/latin-char`
(开放标点 hold-and-prepend;闭合标点 append-to-prev)。
**已知老 bug**:连续闭合标点(字。」)第二个独立成盒且断点未禁止 →
」可出现行首;开放标点跨空格 hold 还会导致盒序与原文顺序不一致。
迁移后两者都根治。
- `ekp--str-type` 返回 space/latin/cjk/cjk-punct → 拆成 cjk-open
(opening-punct-p:general-category Ps/Pi)/ cjk-close(fw-punct-p
且非 open)。“” 特例保持 'cjk。
- `ekp--glue-type` 新矩阵(保持旧拓扑等价):space→nws;
before=open→nws(原盒内);after=close→nws(原盒内;close-close
从 cws 改为 nws,属有意修正);latin-latin→lws;cjk-cjk→cws;
cjk/latin 混→mws;其余含标点→cws。
- breaks-allowed 规则:`allowed[k] = !(tail(box[k-1])=open || head(box[k])=close)`,
k∈[1,n-1];k=n(段末)恒可。存 bool-vector(elisp DP 用)+
forbidden-positions int 向量(C 打包用,稀疏,仿 hyphen-positions)。
- DP 改动(elisp `ekp--dp-run-1d` + C `dp_process_position` 镜像):
候选 k 需 allowed;不 allowed 时**不 throw**继续延伸;
紧急兜底从 single-box 推广为 atomic-run(i 到 k 间无允许断点);
多盒紧急行需记录 gaps 计数(渲染 normal 路径 clamp ≥0 自然溢出)。
- C 桥:`ekp-c-break-with-arrays` 11→12 参(forbidden-positions),
batch 向量同步;`ekp-c-set-penalties` 后续 ragged 加 extra-stretch
标量;protrusion 再加两数组(head/tail protrude px)。每次 API 变
动 bump EKP_VERSION_MINOR + `ekp-c-module-required-version`
- 隐性收益:「Hello / Hello」 之前整盒无法匹配断词正则(左右标点类
不含 CJK 引号)→ 拆盒后可正常断词。
- 测试影响:tests/ekp-tests.el 里 split-* 结构测试要改为新盒契约;
新增行为级禁则测试(任意宽度:行首无 close、行尾无 open、
字。」不拆)。fuzz 断言与引擎无关,应保持 0 失败。
## 阶段与提交计划(全部完成 2026-07-26)
- [x] A 地基 64eb2f3:标点成盒 + breaks-allowed + DP/C 1.2;顺带修复
连续闭合标点行首漏洞、open-punct 跨空格盒序错乱、「Hello 断词失效;
半角标点禁则(纯标点盒判定)
- [x] B ea96a6d:ekp-no-break 属性(刚性原子/禁断词)、NBSP/NNBSP/
FIGURE SPACE/WJ/ZWNBSP、命令 ×2;零 C 改动
- [x] C 57a3abe:ekp-alignment 四模式 + ekp-ragged-stretch-pixel;
C 1.3(set-penalties 第 7 参 extra-stretch,缺省归零)
- [x] D f6aa64b:ekp-protrusion 右缘悬挂(cjk-close/latin-close/hyphen
比率);DP/渲染/C 重建三处 lw=width+release 同步;C 1.4
(break-with-arrays 14 参);region 预留 protrusion-reserve;
仅右缘(左缘无法渲染,文档已注明)
- [x] E 720b1cd:ekp-parshape + ekp-first-line-indent(t=2em 按段落
CJK 字体);loose 2D 每行宽;C 旁路
- [x] F 4d9a018:ekp-verbatim 段落豁免 + ekp-region-skip-faces +
buffer-local skip-predicate;行内原子沿用 ekp-no-break;核心零改动
- [x] G:readme×2 排版特性/verbatim 章节、DEVELOPER×2 §5.1;GUI 目检
(悬挂+缩进+verbatim+auto-mode 齐行/ragged 两态截图确认)
最终状态:66 ERT 全绿,fuzz 300/300(每阶段跑),C 模块 1.4 两引擎
逐字节一致。行中挤压不可渲染(Emacs 无负宽 display)= 已知边界。
## 验证清单(每阶段)
byte-compile 零警告(error-on-warn)→ 47+ ERT → C 重建 + parity →
fuzz 300 → 提交。改 ekp_c/ 后必须 make clean && make。
Emacs: /Applications/Emacs.app/Contents/MacOS/Emacs

View File

@ -1,295 +0,0 @@
# Changelog
All notable changes to emacs-kp are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/), and the
project aims to follow [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Fixed
- Malformed non-nil C backend results no longer fall back to Elisp. Nil whole
results and nil per-item breaks remain soft failures; malformed single or
batch output now signals `ekp-backend-contract-error`, so a broken backend
cannot silently produce a different layout.
- An overwide `ekp-no-break` inline atom no longer collapses its preceding
CJK text into one-glyph emergency lines. The final pass now gives ordinary
underfull candidates finite background emergency stretch and lets normal
badness/fitness/demerits choose the layout. If an overfull candidate would
otherwise extinguish every active path, TeX-style artificial demerits
preserve the best provisional path with zero incremental cost. The rule is
content-independent and identical in the Elisp 1D, looseness/parshape, and
C engines.
- C-backed core layout and complete resize/reflow now reuse prepared
paragraph, DP, and gap geometry and avoid duplicate projection
publication. The four-round same-machine gate records p95 values of
27.687 ms and 27.487 ms respectively, with exact frozen-C/Elisp parity.
- Reprojecting a buffer now preserves the mark marker and `mark-active`
independently. Showcase width keys and resize reflow no longer turn an
old inactive mark into a highlighted region.
- The unfinished live line now keeps its source-edge whitespace natural.
A leading/trailing space or tab appears in the same input turn, and
whitespace exposed by backspace is no longer hidden by `display ""`
until another glyph arrives.
- Enabling or disabling a theme, or changing a frame's default font, now
invalidates live plan history and reflows every active auto-justify
buffer with the new glyph metrics.
- Changing any Knuth-Plass cost parameter now selects a correctly keyed
DP/render result immediately; cached paragraphs no longer require
`ekp-clear-caches`. Structurally equal non-zero-looseness signatures now
reuse their cached result instead of missing an `eql` cons key.
- Changing `ekp-default-cws-stretch-pixel` in automatic spacing mode now
invalidates both paragraph-cache lookup paths immediately. Unchanged
spacing signatures still reuse the cached paragraph.
- Buffer layout no longer rewrites the source character stream. Saving,
region writes, direct Elisp reads, syntax, and search therefore operate
on logical text without a temporary serialization copy.
- Live editing now separates committed projection from a local dirty edit
transaction. Same-row typing performs no whole-hard-line planning,
middle-row edits preserve unaffected break anchors, and exact source
reversal restores the saved projection `equal-including-properties`.
- Point-only motion no longer changes live state or projection anywhere,
including after leaving the active hard paragraph. Structural commits are
owned by visual-row crossing, hard completion, the next real edit
elsewhere, explicit refill, and width/font/layout-context changes.
- Kill/copy integration now composes with and restores an existing
`filter-buffer-substring-function`, including DELETE operations, and
strips only EKP-owned projection properties from copied text. Removing
the final layout span outside auto mode restores the exact prior filter.
- Dictionary syntax no longer degrades silently: files containing
libhyphen replacement/slash patterns fail with an explicit condition
because the fixed-width DP cannot model their conditional rewrites.
- Mutating layout-relevant text properties on an already cached string now
invalidates the same-string paragraph fast path. It reuses the complete
structural cache key instead of a partial parallel signature.
### Changed
- The core now exposes semantic `ekp-layout-plan`, `ekp-layout-line`, and
`ekp-layout-gap` records. The compatible string renderer and the buffer
renderer consume the same break, glue, indentation, and discretionary-
hyphen decisions.
- Buffer/region layout is now a pure text-property projection on existing
source characters: ASCII spaces combine `space-width` and absolute-pixel
`min-width`; zero-source CJK/mixed glue uses `min-width` on a complete
grapheme; indentation uses `line-prefix`; and visual breaks/hyphens use
replacing display strings. EKP creates no overlay and inserts no layout
character into a buffer.
- Auto-justify now uses stable hard-line transactions. A dirty island stays
native between commit events, so ordinary keys neither run DP nor rewrite
unrelated properties. Native visual-row crossing atomically republishes
every completed row from the unchanged whole-hard-line core plan; the new
row remains natural. A buffer-local 16-entry LRU reuses recent
text/context plans, and common-prefix signatures minimize writes at
commits. IME and resize publication remain generation-checked; no
edit-idle or cursor-motion formatter exists.
- Auto-justify now owns its native soft-wrap precondition. It temporarily
disables `truncate-lines` and narrow partial-window truncation, including
Emacs's default sub-50-column behavior, then restores the prior values and
buffer-local ownership on teardown.
- One buffer uses the narrowest displayed window as its authoritative
width. Foreign replacing display owners and unsupported non-ASCII
whitespace shrink keep only the affected hard paragraph natural and are
reported by `ekp-diagnose`.
- Automatic work bounds a single hard paragraph with
`ekp-auto-justify-paragraph-limit` (default 2,048 characters). Longer
paragraphs remain naturally editable; `ekp-refill-paragraph` is the
explicit unbounded quality pass.
- **Breaking:** the editor integration module is now `ekp-buffer.el` and
provides `ekp-buffer`; replace `(require 'ekp-region)` with
`(require 'ekp-buffer)`. Module-owned configuration names now use the
`ekp-buffer-` prefix. Public commands whose names describe an actual
region or buffer operand are unchanged.
- The bundled dictionary inventory is now 49 reproducible entries with a
pinned LibreOffice commit, per-file SHA-256/source/license manifest, and a
portable verifier/exporter. Sanskrit was removed because the pinned
upstream data has no authoritative license statement for that exact
hyphenation file.
- Auto-justify mode now exposes its existing formatting, protection, and
diagnostic commands in an EKP menu. Interactive no-break/verbatim changes
report that their text properties last only for the current buffer
session.
### Tests
- ERT now covers source/tick/undo invariants, exact ASCII and CJK glue
projection, display-only static breaks and hyphens, zero-projection
underfilled input, whole-hard-line semantic-prefix selection, non-frozen
earlier breaks, common-prefix differential writes, plan-cache reuse,
point-motion projection stability, stable dirty transactions, exact
reversible restoration, atomic visual-row commits, hard-paragraph
completion, IME/stale generations, foreign ownership,
narrowest-window policy, inactive/active mark preservation, immediate
edge whitespace, consecutive spaces, newline/yank/real-undo paths, and
the oversized-paragraph guard.
GUI probes verify pixel widths. Reviewed temporal recordings verify
width-key selection and immediate edge-whitespace states, plus stable
middle-row edits, exact reversible restoration, atomic visual-row
commits, zero-work point motion, public yank/undo, resize commits, and
hard-paragraph completion without delayed snap or overlays.
- ERT fixtures now restore every mutable EKP option they isolate. The
parshape C-bypass regression drives the public formatter, and reusable
permuted-order plus per-test fresh-process runners prevent alphabetical
execution from hiding leaked state.
- The GUI fit matrix now exits with status 1 when any row fails. A
batch-safe forced-failure control locks the automation contract while the
real seven-case matrix remains an explicitly loaded developer tool.
- Release invariants now have one local gate. CI action inputs are immutable,
Windows runs the Elisp baseline, and the `.phrase` decision source is
versionable instead of ignored.
### Performance
- Live structural commits now extend prepared paragraph data from the last
complete-word boundary, resume pure-Elisp DP from a safe retained state,
reuse unchanged layout lines, and reconstruct only the buffer's dirty
source island. Three repeated byte-compiled public-command runs at 80
pixels measure append p99 at 1.1581.326 ms for C and 1.4291.438 ms for
pure Elisp; same-row editing and point motion still perform zero planning.
The stricter frozen source-instrumented matrix improves C p99 by 78.37% to
25.785 ms and Elisp p99 by 92.03% to 47.578 ms, but remains openly red
against its locked 16 ms absolute stress target (`issue018`/`task030`).
No stale reuse, debounce, skipped publication, or global GC workaround
hides either result.
- Tokenization now accumulates fragments and joins once per emitted box;
dense hyphen insertion likewise joins original word slices once. On the
1,0008,000-character adversarial benchmark, the 8,000-character cases
improved from 3.133 s to 1.100 s and from 0.945 s to 0.013 s respectively.
- Cached “no hyphen” results now use an explicit miss sentinel, so repeated
lookups do not rerun the dictionary computation.
### Internal
- Cached semantic layout plans now return consumer-owned copies of all
plan-owned mutable payloads: strings, context, boxes, offsets, line/glue/
gap records, and signatures. Layout context snapshots and returned contexts
recursively copy conses, vectors, and strings, so mutable policy inputs
cannot alias the cache key. The paragraph object remains intentionally
shared for stable append identity.
- DP reuse identity, edge-space exclusion, and the lossless marker
vocabulary now each have one directly tested rule owner. The consolidation
removes formula/property-list drift without adding modules or hot-loop
allocations.
- Final cleanup removed nine dead pass-through accessors and replaced direct
use of Emacs's private substring-filter dispatcher with the public
`filter-buffer-substring` path while preserving DELETE lifecycle ownership.
- The paragraph fast path no longer needs style-variable watchers: both the
one-entry path and hash cache have one structural identity owner.
### C module (1.6)
- Signed 32-bit module inputs now use one `extract_integer` validation
instead of calling Lisp `integerp`, `>=`, and `<=` for every value. The
frozen 80-pixel candidate module layer measures 0.697/0.701 ms p95/p99,
down from 2.615/2.655 ms, without changing the ABI or DP output.
- The 15-field single and batch APIs now preflight vector shape, lengths,
scalar types, and signed 32-bit input range before extraction. Caller
errors signal `ekp-c-invalid-input`; allocation/no-result still returns
nil. Enabled-backend signals are no longer swallowed by the Elisp
dispatcher.
- Line metric, flexibility, and remaining-space intermediates are now
64-bit, eliminating signed overflow when valid int32 widths and
protrusions combine.
- The default C build is portable (`PROFILE=portable`). Native CPU/LTO,
debug, and sanitizer flags are explicit profiles, and
`ekp-c-module-build` now passes argv directly to `make` from the module
directory instead of interpolating a shell command.
## [1.0.0] - 2026-07-27
First tagged release. Highlights of the work leading up to it:
### Editor integration
- Saving an auto-justified buffer now writes the **logical** text to
disk; soft line breaks, glue spaces and break hyphens are layout,
not content, and no longer reach the file.
- `isearch` searches the logical text, so CJK phrases and hyphenated
words are found across the justified layout.
- Copying/killing puts the logical text on the kill ring (via a
buffer-local `filter-buffer-substring-function`).
- Justification no longer flips `buffer-modified-p` on an unmodified
buffer — no stray lock files or auto-saves — and re-flow no longer
fights `undo`.
- Text typed adjacent to synthesized glue/hyphens no longer inherits
the marker properties (`rear-nonsticky`), so it is never mistaken
for layout and deleted on the next re-flow.
- Switching major mode or turning the minor mode off restores the
logical text cleanly, widening first so narrowing leaves no
justified orphans.
- Paragraphs containing `field`/`read-only` text (comint prompts) are
left verbatim.
### New commands and options
- `ekp-justify-buffer` / `ekp-unjustify-buffer`.
- `ekp-justify-region` / `ekp-unjustify-region` act on the paragraph
at point when no region is active (DWIM).
- `ekp-org-setup` / `ekp-markdown-setup` wire the verbatim-protection
presets in one call; `ekp-auto-justify-mode` applies the matching
one automatically in Org/Markdown buffers.
- `fill-paragraph` is remapped to `ekp-refill-paragraph` while the
mode is on.
- User options are now `defcustom`s under the `ekp` / `ekp-region`
groups.
- `ekp-cjk-no-line-start-extra`: Japanese line-start prohibition for
small kana, the prolonged sound mark and iteration marks (JIS X
4051).
- `ekp-auto-justify-tick-budget`: time budget per background re-flow
tick.
### Typography
- First-line indent (`ekp-first-line-indent`) now runs on the fast 1D
dynamic program and the C engine, instead of the heavy 2D path.
- Dictionary `LEFTHYPHENMIN` / `RIGHTHYPHENMIN` are honored (English
now keeps ≥2 before and ≥3 after a break, per its dictionary).
### Performance
- Session-global box-width cache eliminates cross-paragraph duplicate
measurement (≈40% of `string-pixel-width` calls on the Chinese
sample); byte-compiled justify of that sample dropped ~40%.
- Cache keys ignore volatile `fontified` bookkeeping, restoring the
paragraph-cache hit rate in font-locked buffers.
- Large-buffer re-flow: huge edits chunk in the background, scrolled
regions get priority, background ticks honor a time budget.
### Fixed
- The DP cache no longer returns a stale layout after `ekp-looseness`
changes (results are keyed by looseness).
- Changing a style variable (`ekp-alignment`, …) invalidates the
same-string fast path.
- Missing `dictionaries/` directory only disables hyphenation instead
of breaking `(require 'ekp)`.
### C module (1.4 → 1.5)
- **Breaking:** the unused experimental C tokenization path
(`ekp-c-break-lines`, `ekp-c-hyphenate`, `ekp-c-load-hyphenator`,
`ekp-c-set-spacing`, and the `ekp_paragraph.c` / `ekp_hyphen.c`
sources) is removed. It carried a heap overflow reachable from
Lisp. Rebuild with `make -C ekp_c clean all`.
- `ekp-c-break-with-arrays` gains a 15th argument, `FIRST-LINE-WIDTH`,
supporting first-line indent in C.
- Thread pool sized to the machine's cores, created lazily; a full
queue blocks instead of dropping tasks.
- Allocation failures and bad arguments fail the call cleanly (Elisp
fallback) instead of silently producing a different layout;
extracted integers are clamped to int32.
### Packaging
- Added `COPYING` (GPL-3.0-or-later) and license headers to all
sources; real author/maintainer metadata.
- Autoload cookies on all interactive commands.
- Removed internal working files and an unused demo GIF from the repo.
## Historical
Earlier work (the core KP overhaul and the P1/P2 typography wave —
optimal line breaking, CJK kinsoku, hyphenation, pixel-exact
justification, alignment modes, hanging punctuation, parshape,
no-break atoms, verbatim protection, the C module) predates this
changelog; see the git history.

View File

@ -1,74 +0,0 @@
# Contributing to emacs-kp
Thanks for your interest. This document covers the essentials for
making a change that will pass review and CI.
## Development setup
```bash
EMACS=emacs # or /path/to/Emacs
# Byte-compile with warnings as errors (CI does this)
$EMACS -Q --batch -L . \
--eval '(setq byte-compile-error-on-warn t)' \
-f batch-byte-compile ekp.el ekp-utils.el ekp-hyphen.el ekp-buffer.el
# Run the ERT suite (C-module tests auto-skip if not built)
tests/run-tests.sh $EMACS
# Build the C module (required for the parity tests and fuzz)
make -C ekp_c PROFILE=portable
# Property fuzz: 300 random cases, asserts C and Elisp agree byte-for-byte
$EMACS -Q --batch -L . -l tests/ekp-fuzz.el
# Check release/CI/version invariants
tests/check-release.sh
# Check dictionary inventory and pinned upstream bytes
tests/check-dictionaries.sh
dictionaries/update.sh check
```
## Ground rules
- **The two engines must produce byte-identical output.** Any change
to the demerits or line-metric formulas must touch both
`ekp--dp-run-1d` (Elisp) and `dp_process_position` (`ekp_c/ekp_kp.c`),
and the fuzz suite must stay at 300/300.
- **The two renderers have different rights.** The string renderer's
physical marker vocabulary (`ekp-glue`, `ekp-soft-break`,
`ekp-soft-hyphen`, `ekp-hidden`) stays lossless and compatible. The
buffer renderer must create no overlay or source character and may use
only EKP-owned text properties on existing characters.
- Any C-module API change bumps `EKP_VERSION_MINOR` and the matching
`ekp-c-module-required-version`, and rebuilds the module.
- New behavior needs an ERT test. Buffer-level behavior (save,
isearch, undo, kill/yank, mode interactions) goes in
`tests/ekp-buffer-tests.el`.
## Style
- `lexical-binding: t` everywhere; keep byte-compilation warning-free.
- `checkdoc` clean (CI enforces it): imperative docstring first lines,
arguments mentioned in uppercase, two spaces after a sentence.
- `package-lint` clean: the `ekp-` / `ekp-buffer-` namespaces, proper
autoload cookies on interactive entry points.
- Match the surrounding code; keep comments about *why*, not *what*.
## Commits
Conventional Commits (`feat:`, `fix:`, `perf:`, `refactor!:`,
`docs:`, `test:`, `chore:`). Explain the reasoning in the body, not
just the change.
## Releases
Follow [Docs/RELEASING.md](Docs/RELEASING.md). In particular, action
dependencies stay pinned to full commit SHAs, released artifacts are
immutable, and every artifact gets a SHA-256 checksum.
## License
By contributing you agree that your contributions are licensed under
GPL-3.0-or-later, matching the project (see `COPYING`).

674
COPYING
View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -4,9 +4,6 @@ This document describes the internal architecture, algorithms and APIs of
`emacs-kp`, as implemented. It is intended for contributors and advanced `emacs-kp`, as implemented. It is intended for contributors and advanced
users. users.
Current repository health and prioritized follow-up work are recorded in
the [2026-07-28 system audit](./Docs/REPOSITORY_AUDIT_20260728.md).
## 1. Pipeline Overview ## 1. Pipeline Overview
A justification call flows through five stages: A justification call flows through five stages:
@ -58,26 +55,17 @@ Everything the DP and renderer need, computed once per paragraph:
| `lead-spaces` | `lead-spaces[i]` = width of the space-box run starting at box i; index 0 forced to 0 (first-line indentation is kept) | | `lead-spaces` | `lead-spaces[i]` = width of the space-box run starting at box i; index 0 forced to 0 (first-line indentation is kept) |
| `trail-spaces` | `trail-spaces[k]` = width of the space-box run ending at box k1 | | `trail-spaces` | `trail-spaces[k]` = width of the space-box run ending at box k1 |
| `glue-params` | plist snapshot of the nine spacing values at creation time | | `glue-params` | plist snapshot of the nine spacing values at creation time |
| `dp-cache` | equal-keyed hash: complete DP signature → dp-result plist | | `dp-cache` | hash: line-width → dp-result plist |
The paragraph cache (`ekp--para-cache`) is keyed with `equal` on a The paragraph cache (`ekp--para-cache`) is keyed with `equal` on a
structured key — string content, printed text-property intervals, structured key — string content, printed text-property intervals,
detected fonts, the hyphenation language (`ekp-latin-lang`), and detected fonts, the hyphenation language (`ekp-latin-lang`), and
either the nine explicit spacing values or the automatic CJK stretch either the nine explicit spacing values or the symbol `auto`.
default. The other automatic values are derived from font measurement,
which is already represented by the font and display-context fields.
Structured keys make hash collisions harmless (they were possible with Structured keys make hash collisions harmless (they were possible with
the previous `sxhash`-integer scheme). The cache is flushed when it the previous `sxhash`-integer scheme). The cache is flushed when it
exceeds `ekp-para-cache-limit`. A one-entry fast path exceeds `ekp-para-cache-limit`. A one-entry fast path
(`ekp--last-para`) bypasses only the hash lookup: it compares the same (`ekp--last-para`, checked by string `eq` + language) covers the many
complete structural key before reuse. In-place changes to layout-relevant same-string lookups inside one justification call.
text properties therefore miss both paths and match a fresh paragraph.
The DP signature is separate from the paragraph key. It contains line
width, looseness, and all six runtime cost parameters; therefore a
parameter change selects a new result without flushing width-independent
paragraph data. Structural `equal` comparison also lets non-zero
looseness signatures hit the cache.
### dp-result ### dp-result
@ -100,8 +88,6 @@ computed for ideal, min and max in O(1). Space-box runs at the line
edges are excluded because the renderer strips them; the DP and the edges are excluded because the renderer strips them; the DP and the
renderer therefore agree exactly, and every justified line renders at renderer therefore agree exactly, and every justified line renders at
precisely the target width (`ekp-test-justify-line-width-invariant`). precisely the target width (`ekp-test-justify-line-width-invariant`).
`ekp--line-stripped-space-pixel` owns this exclusion rule for the 1D/loose
DP, C-result reconstruction, and renderer.
## 4. The Knuth-Plass DP ## 4. The Knuth-Plass DP
@ -121,12 +107,10 @@ badness = min(10000, 100·|adjustment/flexibility|³)
``` ```
Fitness classes (tight/decent/loose/very-loose) follow the TeX ratio Fitness classes (tight/decent/loose/very-loose) follow the TeX ratio
thresholds. Special cases: single-box lines use flexibility 1 in the thresholds. Special cases: single-box lines use flexibility 1 and
strict pass; the final pass uses the same finite emergency stretch as fitness decent; the last line pays `(line-penalty + short-badness)²`
ordinary underfull lines. The last line pays where `short-badness = last-line-short-penalty × (1 fill)` when the
`(line-penalty + short-badness)²` where fill ratio is below `ekp-last-line-min-ratio`.
`short-badness = last-line-short-penalty × (1 fill)` when the fill ratio
is below `ekp-last-line-min-ratio`.
Deviations from the 1981 paper, by design: penalties are always added Deviations from the 1981 paper, by design: penalties are always added
as `+p²` (no negative/flagged penalties), there is no `q`/looseness in as `+p²` (no negative/flagged penalties), there is no `q`/looseness in
@ -137,22 +121,12 @@ the main pass (see §6), and adjacent-fitness is a flat constant.
Some inputs admit no valid layout: an unbreakable box wider than the Some inputs admit no valid layout: an unbreakable box wider than the
line, or a rigid (all-`nws`) region that cannot stretch to the target. line, or a rigid (all-`nws`) region that cannot stretch to the target.
A strict pass runs first; if the paragraph end is unreachable, a second A strict pass runs first; if the paragraph end is unreachable, a second
pass adds a finite background emergency stretch to ordinary underfull pass additionally allows **emergency breaks** — single-box lines with
candidates and still scores them through the same adjustment ratio, demerits `(line-penalty + 10000)² + rest²`, at least as bad as any
badness, fitness, and demerits used by the strict pass. That keeps regular line. This guarantees, by induction over positions, that every
underfull final-pass choices inside the global K-P DP instead of forcing input produces output (regression: narrow CJK used to return an empty
them through a separate fixed-cost path. string), while the common case pays nothing and keeps pure K-P
optimality. Both engines implement the identical strategy.
Separately, the final pass implements TeX's `artificial_demerits` safeguard.
When an overfull candidate would otherwise remove the last active path to a
breakpoint and no non-overfull candidate survives there, the best provisional
path is installed with tight fitness and zero incremental demerits. This is a
reachability rule, not a hard-atom scoring shortcut: it never competes while a
normal active path survives, and it does not inspect CJK, units, or token
classes. Thus every input still produces a complete plan (regression: narrow
CJK used to return an empty result), while ordinary underfull lines compete
by normal K-P cost. The Elisp 1D, looseness/parshape, and C engines implement
the identical strategy.
## 5. Rendering ## 5. Rendering
@ -165,213 +139,14 @@ the identical strategy.
- Last lines are ragged-right (ideal glues + trailing filler); - Last lines are ragged-right (ideal glues + trailing filler);
single-box lines get a trailing filler clamped at ≥ 0. single-box lines get a trailing filler clamped at ≥ 0.
`ekp-layout-plan` is the representation boundary between layout and `ekp--pixel-justify` then strips leading space boxes (except on the
rendering. It combines the paragraph boxes/source offsets, DP breaks, first line — indentation) and trailing space boxes, and appends a
per-line glue targets, indentation, stripped edges, and discretionary hyphen — propertized like the word it breaks — where a line ends at a
hyphen decisions into `ekp-layout-plan`, `ekp-layout-line`, and hyphenation point. Stripped widths are *not* redistributed: the DP
`ekp-layout-gap` records. The plan contains no buffer positions or display already excluded them (§3).
mechanism. `ekp-render-layout-string` consumes it for the public string API;
the buffer integration can consume the same decisions without re-running or
reinterpreting the KP algorithm.
Cached semantic plans are immutable to the cache owner. A cache hit returns Glues become `(space :width (N))` display properties, so justification
a consumer-owned copy of every plan-owned mutable payload: source string, is pixel-exact in GUI Emacs and column-exact in batch/tty.
context tree, box vector and box strings, source offsets, line records,
line glues, gap records, and line signatures. `ekp-layout-plan-para` is the
intentional exception: paragraph-cache ownership predates the semantic-plan
cache, and append planning depends on stable paragraph identity.
Layout context snapshots and returned plan contexts use the same recursive
copier for conses, vectors, and strings, so mutable policy inputs cannot alias
the cache key or a later consumer plan.
The two consumers deliberately have different representation rights.
#### String renderer
`ekp-render-layout-string` preserves the public string API. It strips
leading/trailing space boxes, synthesizes display spaces and visual
newlines, and appends a propertized discretionary hyphen where selected.
The returned string remains lossless through four private markers:
| property | on | value / meaning |
|-------------------|------------------------|-------------------------------|
| `ekp-glue` | synthesized glue space | original text it replaced |
| `ekp-soft-break` | synthesized `\n` | swallowed boundary whitespace |
| `ekp-soft-hyphen` | synthesized hyphen | marker only |
| `ekp-hidden` | paragraph-edge text | source retained, display empty|
This physical representation exists only in the returned string. It is
kept for API compatibility and is not installed into a source buffer.
`ekp--layout-marker-properties` owns its marker vocabulary and
non-inheritance contract.
#### Buffer renderer
`ekp-buffer.el` keeps the buffer's character sequence untouched and
projects the same plan with text properties on existing source characters:
- source ASCII spaces:
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
- zero-source CJK/mixed glue: `min-width` on the preceding complete
grapheme, targeting its natural advance plus glue;
- indentation: `line-prefix`;
- a source-whitespace break: the first boundary character displays as
newline and the rest as empty;
- a CJK or discretionary-hyphen break: a replacing display string
reproduces the existing complete grapheme, appends the optional hyphen,
then a visual newline.
`ekp-buffer--display` and `ekp-buffer--line-prefix` record exact ownership.
Removal clears the public property only when its value is still identical
to EKP's owner value, so a later foreign change is not erased. Paragraphs
with foreign `display`, `line-prefix`, `wrap-prefix`, `composition`, or
`invisible` ownership stay verbatim. Exact shrink of tabs/non-ASCII
whitespace is also refused because `space-width` affects ASCII spaces only.
All installation/removal runs inside `with-silent-modifications`, and owned
properties are nonsticky. Buffer characters, point/mark, modified state,
undo, character-modified tick, and external change hooks therefore remain
source-owned. Reprojection restores the mark marker without calling
`set-mark`, then restores `mark-active` independently; an inactive mark
cannot become a region as a layout side effect. No EKP buffer path creates
an overlay.
Saving, ordinary search, syntax, and direct Elisp character APIs need no
logical-text adapter: the real buffer is already logical. Copy filtering
remains necessary because `buffer-substring` intentionally preserves text
properties. EKP composes with the previous
`filter-buffer-substring-function`, then removes only its projection
metadata from the copied string.
#### Live flow
Live editing uses the ordinary whole-text Knuth-Plass plan without giving
editing state to the core planner:
1. Live state owns the last committed hard-line source, normal
`ekp-layout-plan`, semantic signatures, projected spans, and stable break
anchors at the narrowest-window authoritative width.
2. The first real change opens one edit transaction. It snapshots that
committed state, then removes EKP properties only from the smallest
projected span range containing the edit. Span objects and unaffected
anchors remain registered.
3. Further changes inside the dirty island use native soft wrapping and do
no whole-hard-line planning. If the logical source returns exactly to the
snapshot, EKP restores the saved owned-property runs and marker offsets
directly; the state, plan, signatures, and spans retain object identity.
4. Crossing a native visual-row boundary is a commit. EKP computes or reuses
one whole-hard-line plan, derives every completed semantic row, and
publishes their changed suffix silently as one command-loop transition.
The new current row remains natural.
5. The other commits are a hard newline/paragraph completion, the next real
edit outside the dirty island, explicit refill, and width/font/face/theme
or layout-context change. Point motion is never a commit, even across hard
paragraphs; there is no live `post-command-hook`.
6. Stable line signatures minimize property writes at a commit, while the
16-entry buffer-local LRU reuses recent text/context plans. Neither
mechanism decides when layout is allowed to change; the transaction owns
that policy.
7. IME preedit, foreign display ownership, unsupported shrink, oversized
hard lines, stale generations, or publication errors fail closed to
native display. A partial projection is rolled back and the original
error is surfaced.
Native wrapping is a state-machine precondition, not a user preference the
mode can merely hope is enabled. On activation, the mode snapshots the
values and local-binding ownership of `truncate-lines` and
`truncate-partial-width-windows`, then makes both buffer-local and nil.
Teardown restores local values or removes the temporary bindings so global
ownership resumes. This prevents Emacs's default 50-column partial-window
threshold from silently turning a narrow split into horizontal scrolling.
This state model needs no live lookahead, push/pull convergence, per-key
whole-line planner, or idle formatter. A real edit after point motion may
commit the previous active hard line, including when narrowing makes that
line inaccessible; point motion itself remains a strict no-op.
No edit-idle whole-paragraph formatter exists. Resize/background work is
generation-owned. Large buffers are processed visible-first in hard-
paragraph chunks; a single hard paragraph above
`ekp-auto-justify-paragraph-limit` remains natural during automatic work
and requires explicit `ekp-refill-paragraph` for an unbounded quality pass.
Because text properties are buffer-wide, the narrowest live window supplies
the one authoritative width.
### 5.1 Break permissions, alignment, protrusion, shapes
- **Break permissions**: every CJK char (punctuation included) is its
own box; `ekp-para-breaks-allowed` forbids gaps per kinsoku (full-
and halfwidth), `ekp-no-break' spans and NBSP-family joiners.
Forbidden gaps carry no glue. The DP skips them as candidates while
the line keeps extending. In the final pass, legal underfull candidates
receive finite emergency stretch and are scored by normal
badness/demerits. If an overfull candidate would extinguish the last active
final-pass path, TeX-style artificial demerits preserve that path with zero
incremental cost. C receives the sparse `forbidden-positions` vector.
Explicit hard atoms forbid only breaks inside their interval; an otherwise
legal boundary immediately before or after an atom remains legal. Atom
adjacency therefore receives no special scoring or break prohibition.
- **Configurable policy compilation**: buffer and core policy variables are
resolved before tokenization into private structural intervals. Region
`ekp-break-policy` wins first, explicit buffer/file/dir locals win over
mode profiles, and profiles win over global defaults. Token policies are
the only merged category map; scalar and face-list options replace the
lower scope. The core may use private properties such as
`ekp--face-break-policy`, `ekp--no-hyphen`, and
`ekp--literal-spacing` on an analysis copy, but cache keys, plan strings,
boxes, rendered strings, and `ekp--last-para` must contain only public
source properties. Automatic face no-break is measured over the
contiguous private face-policy span and downgrades to no-hyphen when
overwide; explicit `ekp-no-break` never downgrades.
- **Literal inline spaces**: face-derived `no-hyphen` preserves source
spaces as literal boxes inside a line, forbids a break that would move a
literal source-space box to line start, and permits the complementary
break after a source-space box. When such whitespace is the selected
visual break, source ownership belongs to the break gap metadata, not to a
trailing visible line box. This rule is policy-derived and must not depend
on whether the source also carries a public `face` property.
- **Alignment** (`ekp-alignment`): non-justify modes zero the glue
stretch/shrink arrays and class params; the DP widens `max_w` by an
extra per-line stretch R (`ekp-c-set-penalties` arg 7), so badness =
100·(shortfall/R)³. The renderer places each line's leftover per
mode (trailing / split / leading).
- **Protrusion** (`ekp-protrusion`): per-gap `tail-protrudes[k]` (the
last non-space box's allowance, looked through trailing spaces) and
a `hyphen-protrude` scalar widen each candidate's effective target
(`lw = width + release`) in the DP, in `ekp-line-glues', and in the
C-result reconstruction — all three must stay in lockstep.
- **Per-line widths** (`ekp-parshape' / `ekp-first-line-indent'):
resolved by `ekp--line-spec' (line-index → INDENT . WIDTH). A
plain first-line indent only changes line 0, and a line begins at
box 0 exactly when the DP start i = 0, so the 1D pass (and the C
engine, via `FIRST-LINE-WIDTH') handle it with no extra state.
Only full `ekp-parshape' and `ekp-looseness' need the
(position × line-count) DP and bypass C. Indents render as leading
`ekp-glue' spacers.
C module 1.6: `ekp-c-break-with-arrays' takes 15 args
(…, forbidden-positions, tail-protrudes, hyphen-protrude,
first-line-width); batch vectors have 15 elements;
`ekp-c-set-penalties' takes 48. Policy compilation feeds the existing
hyphen-position and forbidden-break vectors; it must not add a sixteenth C
argument or batch field without a new architecture decision.
Performance after the feature wave (byte-compiled + C, Apple
Silicon, batch): justify zh w=200 ≈ 54 ms, range zh ≈ 117 ms —
justify at parity with the pre-feature numbers, range ≈ +55 % from
the larger box count. Hot-path caches: per-char `ekp--str-type'
memo, interned glue strings, per-(paragraph, width) rendered-output
cache in the dp-cache (capped at 64 widths). Continuous-reflow
reality check (60-paragraph, 26 k-char article, region layer
included): ≈ 73 ms per width change cold, less on revisit;
incremental single-paragraph re-justify after an edit ≈ 17 ms.
Adversarial source-mode builders are measured separately by
`ekp-bench-adversarial-builders`. From 1,000 to 8,000 characters, the
fragment-based tokenizer grew 6.3× and dense insertion 7.7× (near the
expected linear 8× input growth). At 8,000 characters they took 1.100 s and
0.013 s, versus 3.133 s and 0.945 s before the change. The hyphen-position
cache uses an explicit miss sentinel so a legitimate nil result is reusable.
## 6. Looseness ## 6. Looseness
@ -384,60 +159,33 @@ engines never disagree.
## 7. C Module Integration ## 7. C Module Integration
The C module (`ekp_c/`, version 1.6) runs only stage ④. Elisp remains The C module (`ekp_c/`, version 1.1) runs only stage ④. Elisp remains
the source of truth for all font-dependent data. the source of truth for all font-dependent data.
- `ekp-c-break-with-arrays` (15 args): the para's prefix arrays, glue - `ekp-c-break-with-arrays` (11 args): the para's prefix arrays, glue
arrays, hyphen data, line width, the two space-run arrays, the arrays, hyphen data, line width and the two space-run arrays.
forbidden/protrusion arrays and the first-line width. Returns Returns `(breaks . cost)`.
`(breaks . cost)`. - `ekp-c-break-batch`: a vector of 11-element vectors, processed in
- `ekp-c-break-batch`: a vector of 15-element vectors, processed in
parallel by a pthread pool — one task per paragraph (that is the parallel by a pthread pool — one task per paragraph (that is the
correct granularity; the DP itself is sequential by nature). The correct granularity; the DP itself is sequential by nature).
pool is created lazily on the first multi-paragraph batch and sized - `ekp-c-set-penalties` (46 args): called by `ekp--c-sync-params`
to the machine's cores; a full queue blocks the submitter rather
than dropping the task.
- `ekp-c-set-penalties` (48 args): called by `ekp--c-sync-params`
before *every* C entry, so `ekp-line-penalty` & friends always take before *every* C entry, so `ekp-line-penalty` & friends always take
effect (regression: they were never synced before). effect (regression: they were never synced before).
- `ekp-c-module-load` refuses modules older than - `ekp-c-module-load` refuses modules older than
`ekp-c-module-required-version` and falls back to Elisp, preventing `ekp-c-module-required-version` and falls back to Elisp, preventing
arity mismatches after upgrades. arity mismatches after upgrades.
An unavailable module, an incompatible module version, a nil whole C result, Any C failure (NULL result) silently falls back to the Elisp engine.
or a nil per-item/break result falls back to the Elisp engine. Invalid The two engines are verified to produce byte-identical output by
direct API input signals `ekp-c-invalid-input`. Any non-nil malformed `ekp-test-c-parity-simple` / `ekp-test-c-parity-files`.
backend result signals `ekp-backend-contract-error`: malformed cons shape,
non-list breaks, non-integer/out-of-range/non-increasing/partial breaks,
nonnumeric cost, or malformed batch result shape. Any signal from an
enabled backend propagates through the public formatter; the dispatcher does
not catch and hide it. The module never silently produces a different
layout on partial failure. The two engines are verified byte-identical by
`ekp-test-c-parity-simple` / `ekp-test-c-parity-files` and the 300-case
property fuzz.
### Future direction: a paragraph-handle API `ekp-c-break-lines` (C-side tokenization via `ekp_paragraph.c` and
`ekp_hyphen.c`) is an experimental, self-contained path that ekp.el
Each `ekp-c-break-with-arrays` call re-marshals the paragraph's does not use; see `ekp_c/README.md`.
width-independent arrays (≈18·n `env` extractions). This is invisible
for a single justify but dominates `ekp-pixel-range-justify`, which
re-marshals the same arrays once per candidate width: with a warm
paragraph cache the C path still costs ≈12 ms per width, most of it
marshal, not DP (the DP is ≈2.5 ms for the whole sample).
The fix is a `make_user_ptr` handle: `ekp-c-para-upload` copies the
arrays into a C struct once and returns a handle with a GC finalizer;
`ekp-c-break (handle, width)` then passes only the two width-dependent
scalars. It is deliberately **not** part of this release — it is a
breaking (2.0) ABI change introducing C-side object lifetime, and the
common interactive paths (single justify, `ekp-auto-justify-mode`)
already avoid the repeated marshal because they hit the dp-cache. It
is the clear next step whenever range search or very large batches
become a bottleneck.
## 8. Hyphenation (ekp-hyphen.el) ## 8. Hyphenation (ekp-hyphen.el)
Liang's ordinary pattern algorithm: Liang's pattern algorithm, Pyphen-compatible:
- `dictionaries/hyph_*.dic` are compiled to a pattern hash on first - `dictionaries/hyph_*.dic` are compiled to a pattern hash on first
use and cached per path. Files may be UTF-8 or ISO-8859 (Emacs use and cached per path. Files may be UTF-8 or ISO-8859 (Emacs
@ -445,16 +193,6 @@ Liang's ordinary pattern algorithm:
- `ekp-hyphen-create LANG` resolves exact codes, then progressively - `ekp-hyphen-create LANG` resolves exact codes, then progressively
shorter prefixes (`"de_CH" → "de"`). shorter prefixes (`"de_CH" → "de"`).
- Margins default to 2 characters on each side of a break. - Margins default to 2 characters on each side of a break.
- A slash in a non-comment pattern is fail-closed. Libhyphen replacement
rules change the visible text and width only when their break wins; EKP's
current fixed-width boxes cannot represent that honestly. The compiler
counts the rules and signals `ekp-hyphen-unsupported-pattern`, and the
public formatter propagates it.
- `dictionaries/MANIFEST.tsv` pins 49 inventory entries to one LibreOffice
commit (plus one explicitly identified legacy Basque byte), their SHA-256,
syntax flag, and license evidence. `tests/check-dictionaries.sh` is the
offline gate; `dictionaries/update.sh check` verifies normalized bytes
against the upstream commit on macOS and Linux.
Word boxes are matched against Word boxes are matched against
`^[left-punct]* (latin-word) [right-punct]*$` so that punctuation- `^[left-punct]* (latin-word) [right-punct]*$` so that punctuation-
@ -464,34 +202,12 @@ punctuation stays glued to the first/last syllable box.
## 9. Testing & Benchmarks ## 9. Testing & Benchmarks
```bash ```bash
tests/run-tests.sh [emacs] # batch-safe ERT suite tests/run-tests.sh [emacs] # 36 ERT tests, batch-safe
tests/run-tests.sh [emacs] --random-order
tests/run-tests-isolated.sh [emacs] # each ERT in a fresh process
tests/check-dictionaries.sh # offline inventory/checksum gate
dictionaries/update.sh check # verify pinned upstream bytes
make -C ekp_c PROFILE=portable # release-portable default
make -C ekp_c PROFILE=native # local benchmark only
make -C ekp_c PROFILE=debug # symbols, no optimization
make -C ekp_c PROFILE=sanitize # ASan + UBSan
emacs -Q --batch -L . --eval '(setq ekp-use-c-module nil)' -l tests/ekp-bench.el emacs -Q --batch -L . --eval '(setq ekp-use-c-module nil)' -l tests/ekp-bench.el
emacs -Q --batch -L . --eval '(progn (require (quote ekp)) (ekp-c-module-load))' \ emacs -Q --batch -L . --eval '(progn (require (quote ekp)) (ekp-c-module-load))' \
-l tests/ekp-bench.el -l tests/ekp-bench.el
``` ```
Set `EKP_TEST_SEED` to reproduce or vary the permuted-order run. Test
fixtures dynamically restore all EKP configuration they isolate; tests that
exercise dispatch must use the public formatter rather than only an internal
eligibility predicate.
The GUI matrix is loaded explicitly from `tests/ekp-gui-verify.el`. It
returns status 1 after printing the table when any row fails; the ERT suite
contains a forced-failure control for this boundary.
`M-x ekp-c-module-build` uses the same four profile names and invokes make
as an argv process in `ekp_c/`; it never constructs a shell `cd` command.
Release/CI artifacts use `portable`. Use `native` only for measurements on
the machine that will run the module.
Key invariants under test: rendered line width == target (pixel-exact Key invariants under test: rendered line width == target (pixel-exact
justification), no content loss at any width, brute-force cross-checks justification), no content loss at any width, brute-force cross-checks
of the O(1) prefix machinery, Elisp/C parity on the bundled texts, and of the O(1) prefix machinery, Elisp/C parity on the bundled texts, and
@ -529,12 +245,9 @@ ekp.el Core: para struct, caching, DP (1D + looseness),
ekp-utils.el Tokenizer (boxes, kinsoku), font detection with ekp-utils.el Tokenizer (boxes, kinsoku), font detection with
batch/tty fallbacks, C module loading batch/tty fallbacks, C module loading
ekp-hyphen.el Liang hyphenation + dictionary registry ekp-hyphen.el Liang hyphenation + dictionary registry
ekp-buffer.el Text-property-only buffer/region projection, synchronous
live flow, window lifecycle, copy filtering, diagnostics
ekp_c/ C dynamic module (see ekp_c/README.md) ekp_c/ C dynamic module (see ekp_c/README.md)
dictionaries/ Hunspell hyphenation patterns (from LibreOffice) dictionaries/ Hunspell hyphenation patterns (from Pyphen)
tests/ ekp-tests.el, ekp-buffer-tests.el (ERT), tests/ ekp-tests.el (ERT), ekp-bench.el, ekp-demo.el,
ekp-fuzz.el (parity fuzz), ekp-bench.el, sample texts, run-tests.sh
ekp-demo.el, ekp-showcase.el, sample texts, archive/ Historical prototypes; not loaded, kept for reference
run-tests.sh
``` ```

View File

@ -2,9 +2,6 @@
本文档描述 `emacs-kp` 的实际内部架构、算法与 API,面向贡献者和高级用户。 本文档描述 `emacs-kp` 的实际内部架构、算法与 API,面向贡献者和高级用户。
当前仓库健康度与后续工作的优先级见
[2026-07-28 系统审计](./Docs/REPOSITORY_AUDIT_20260728.md)。
## 1. 处理管线 ## 1. 处理管线
一次排版调用经过五个阶段: 一次排版调用经过五个阶段:
@ -55,20 +52,14 @@ DP 和渲染需要的一切,每段只算一次:
| `lead-spaces` | `lead-spaces[i]` = 从盒 i 开始的连续空格盒总宽;下标 0 强制为 0(首行缩进保留) | | `lead-spaces` | `lead-spaces[i]` = 从盒 i 开始的连续空格盒总宽;下标 0 强制为 0(首行缩进保留) |
| `trail-spaces` | `trail-spaces[k]` = 到盒 k1 结束的连续空格盒总宽 | | `trail-spaces` | `trail-spaces[k]` = 到盒 k1 结束的连续空格盒总宽 |
| `glue-params` | 创建时九个间距值的 plist 快照 | | `glue-params` | 创建时九个间距值的 plist 快照 |
| `dp-cache` | `equal` 比较的哈希:完整 DP 签名 → dp-result plist | | `dp-cache` | 哈希:行宽 → dp-result plist |
段落缓存(`ekp--para-cache`)以 `equal` 比较结构化 key——字符串内容、 段落缓存(`ekp--para-cache`)以 `equal` 比较结构化 key——字符串内容、
文本属性区间的打印形式、检测字体、断词语言(`ekp-latin-lang`)、九个 文本属性区间的打印形式、检测字体、断词语言(`ekp-latin-lang`)、九个
显式间距值或自动 CJK stretch 默认值。其他自动值由字体测量派生,已 显式间距值(或符号 `auto`)。结构化 key 使哈希碰撞无害(旧的 `sxhash`
由字体与显示上下文字段表达。结构化 key 使哈希碰撞无害(旧的 整数方案理论上可能串段)。超过 `ekp-para-cache-limit` 时整体清空。
`sxhash` 整数方案理论上可能串段)。超过 `ekp-para-cache-limit` 单条快路径(`ekp--last-para`,按字符串 `eq` + 语言校验)覆盖同一次
整体清空。单条快路径(`ekp--last-para`)只绕过哈希 lookup,复用前仍 排版内的大量同字符串查询。
比较同一个完整结构 key。因此排版相关 text property 的原地修改会让
两条路径都 miss,并得到与 fresh paragraph 相同的结果。
DP 签名独立于段落 key,包含行宽、looseness 与全部六个运行时代价参数。
参数变化会选择新结果,无需丢弃与宽度无关的段落数据;结构化 `equal`
比较也让非零 looseness 签名能够正常命中缓存。
### dp-result ### dp-result
@ -89,8 +80,6 @@ width = raw space-w (若盒 k1 处断词,再加连字符宽)
理想/最小/最大三个值均 O(1) 得出。行边缘的空格盒串被排除,因为渲染层 理想/最小/最大三个值均 O(1) 得出。行边缘的空格盒串被排除,因为渲染层
会剥离它们;DP 与渲染层因此严格一致,每一行的渲染宽度精确等于目标宽 会剥离它们;DP 与渲染层因此严格一致,每一行的渲染宽度精确等于目标宽
(测试 `ekp-test-justify-line-width-invariant`)。 (测试 `ekp-test-justify-line-width-invariant`)。
`ekp--line-stripped-space-pixel` 统一拥有 1D/loose DP、C 结果重建与
渲染层使用的这一排除规则。
## 4. Knuth-Plass 动态规划 ## 4. Knuth-Plass 动态规划
@ -109,8 +98,7 @@ badness = min(10000, 100·|adjustment/flexibility|³)
``` ```
松紧等级(tight/decent/loose/very-loose)沿用 TeX 的比例阈值。特殊情 松紧等级(tight/decent/loose/very-loose)沿用 TeX 的比例阈值。特殊情
况:严格遍的单盒行 flexibility 固定为 1;最终遍使用与普通欠宽行相同 况:单盒行 flexibility 固定为 1、fitness 为 decent;末行代价为
的有限 emergency stretch。末行代价为
`(line-penalty + 短行badness)²`,填充率低于 `ekp-last-line-min-ratio` `(line-penalty + 短行badness)²`,填充率低于 `ekp-last-line-min-ratio`
`短行badness = last-line-short-penalty × (1 填充率)` `短行badness = last-line-short-penalty × (1 填充率)`
@ -122,17 +110,10 @@ penalty/flagged 断点),主流程无 `q`/looseness(见 §6),相邻松紧惩
某些输入不存在合法排版:比行宽更宽的不可断盒子,或无法伸展到目标宽 某些输入不存在合法排版:比行宽更宽的不可断盒子,或无法伸展到目标宽
的刚性(全 `nws`)区段。先跑严格遍;若段尾不可达,第二遍额外允许 的刚性(全 `nws`)区段。先跑严格遍;若段尾不可达,第二遍额外允许
为普通欠宽候选加入有限 background emergency stretch,并继续走同一套 **紧急断行**——demerits 为 `(line-penalty + 10000)² + rest²` 的单盒行,
adjustment ratio、badness、fitness 和 demerits。这样最终遍的欠宽选择 不低于任何常规行的代价。由位置归纳可证:任何输入必有输出(回归:窄
仍由全局 K-P DP 比较,不会被塞进另一条固定代价路径。 栏 CJK 曾整段返回空串),常规输入不付任何代价、保持纯 K-P 最优。两个
引擎实现完全相同的策略。
另外,最终遍实现 TeX 的 `artificial_demerits` 可达性保护。如果某个
超宽候选将使一个断点的最后活动路径消失,且该断点没有任何未超宽候选
存活,就以 tight fitness 和零增量 demerits 安装最佳暂存路径。这不是
hard atom 专用的评分捷径:只要正常活动路径还在,它就不参与竞争,也不
检查中文、单位或 token 类别。因此任何输入仍会得到覆盖完整源文本的
plan(回归:窄栏 CJK 曾整段返回空结果),而普通欠宽行仍按正常 K-P 代价
竞争。Elisp 1D、looseness/parshape 与 C 引擎实现相同语义。
## 5. 渲染 ## 5. 渲染
@ -144,174 +125,12 @@ plan(回归:窄栏 CJK 曾整段返回空结果),而普通欠宽行仍按正常
≥ 0。 ≥ 0。
- 末行右侧不齐(理想 glue + 尾部填充);单盒行的尾部填充钳制 ≥ 0。 - 末行右侧不齐(理想 glue + 尾部填充);单盒行的尾部填充钳制 ≥ 0。
`ekp-layout-plan` 是布局决策与渲染方式之间的表示边界。它把段落盒子 `ekp--pixel-justify` 随后剥离行首空格盒(首行除外——缩进)与行尾空格
及其原文 offset、DP 断点、逐行 glue 目标、缩进、边缘剥离和可选断词 盒,在断词处附加连字符(继承所断单词的文本属性)。剥离的宽度**不再**
决策组合成 `ekp-layout-plan`、`ekp-layout-line` 和 `ekp-layout-gap` 重新分配:DP 已经排除了它们(§3)。
记录。plan 不包含 buffer 位置或具体显示机制。
`ekp-render-layout-string` 用它服务现有字符串 API;buffer 集成可复用
完全相同的决策,无需重新运行或重新解释 KP 算法。
缓存的 semantic plan 对缓存 owner 不可变。缓存命中会返回 consumer Glue 渲染为 `(space :width (N))` display 属性,GUI 下像素级精确,
自有副本,覆盖所有 plan 自有的可变 payload:源字符串、context 树、盒 batch/tty 下按字符列精确。
向量与盒字符串、源 offset、line 记录、line glue、gap 记录和 line
signature。`ekp-layout-plan-para` 是有意例外:段落缓存所有权早于
semantic-plan cache,append planning 依赖稳定的段落身份。
layout context snapshot 与返回 plan context 使用同一个递归 copier,
覆盖 cons、vector 与 string,因此可变 policy 输入不会别名到 cache key
或后续 consumer plan。
两个消费方拥有不同且明确的表示权限。
#### 字符串渲染器
`ekp-render-layout-string` 保持公开字符串 API 兼容。它剥离首尾空格
盒、合成 display 空格与视觉换行,并在选中断词点附加继承样式的连字符。
返回字符串通过四类私有标记保持无损:
| 属性 | 位置 | 值 / 含义 |
|-------------------|------------------|---------------------------|
| `ekp-glue` | 合成的 glue 空格 | 它所替换的原文 |
| `ekp-soft-break` | 合成的 `\n` | 断点处吞掉的边界空白 |
| `ekp-soft-hyphen` | 合成的连字符 | 仅作标记 |
| `ekp-hidden` | 段落边缘文本 | 保留源文本、显示为空 |
这份物理表示只存在于返回字符串中,用于兼容既有 API,绝不会安装到源
buffer。`ekp--layout-marker-properties` 统一拥有其标记词汇表与不继承
契约。
#### Buffer 渲染器
`ekp-buffer.el` 保持 buffer 字符序列不变,只在现有源字符上用文本属性
投影同一份 plan:
- 源 ASCII 空格:
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
- 无源空格的 CJK/混排 glue:把 `min-width` 加在前一个完整字素上,
目标为其自然 advance 加 glue;
- 缩进:`line-prefix`;
- 源空白断点:第一个边界字符显示成换行,其余显示为空;
- CJK 或拉丁断词断点:replacing display string 重现已有完整字素,
接上可选连字符和视觉换行。
`ekp-buffer--display``ekp-buffer--line-prefix` 记录精确 owner。
移除时只有公开属性的值仍与 EKP owner 值相同时才清除,因此后来的外部
修改不会被误删。带外部 `display`、`line-prefix`、`wrap-prefix`、
`composition``invisible` owner 的段落保持 verbatim。若精确排版
要求缩窄 tab/非 ASCII 空白也会拒绝,因为 `space-width` 只影响 ASCII
空格。
所有安装/移除都在 `with-silent-modifications` 内完成,自有属性设为
nonsticky。因此 buffer 字符、point/mark、modified 状态、undo、字符
修改 tick 与外部 change hook 仍完全由源编辑拥有。任何 EKP buffer
路径都不会创建 overlay。重投影直接恢复 mark marker,不会调用会激活
选区的 `set-mark`,随后独立恢复 `mark-active`;因此布局不能把旧 mark
变成选区。
保存、普通搜索、语法和直接 Elisp 字符 API 不再需要逻辑文本 adapter:
真实 buffer 本来就是逻辑文本。复制过滤仍有必要,因为
`buffer-substring` 按设计保留文本属性。EKP 先组合原有
`filter-buffer-substring-function`,再只移除自己在复制字符串中的投影
元数据。
#### 实时流式排版
实时编辑直接消费正常的整段 Knuth-Plass plan,不会把编辑状态交给
核心规划器:
1. live state 按最窄窗口权威宽度持有最后一次提交的硬行源文本、正常
`ekp-layout-plan`、语义签名、投影 span 与稳定断行锚点。
2. 第一次真实变更打开一个编辑事务。事务先快照提交态,再只移除包含该
编辑的最小投影 span 范围中的 EKP 属性;span 对象与无关锚点仍保持
注册。
3. 脏岛内的后续变更交给原生软折行,不规划整条硬行。若逻辑源文本精确
回到快照,EKP 直接恢复保存的自有属性区间与 marker 偏移;state、plan、
signatures 和 spans 都保持对象身份。
4. 跨过原生视觉行边界才提交。EKP 计算或复用一次整条硬行 plan,得到
全部已完成语义行,并在同一个 command-loop 转换中静默发布真正变化的
后缀;新的当前行保持自然。
5. 其他提交事件只有硬换行/段落完成、下一次真实编辑发生在脏岛之外、
显式 refill,以及宽度/字体/face/主题或布局上下文变化。point 移动永远
不是提交,即使跨越硬段落也一样;live 路径没有 `post-command-hook`
6. 稳定行签名只负责在提交时减少属性写入,buffer-local 16 项 LRU 只负责
复用近期文本/上下文 plan。何时允许布局变化由事务控制,不是二者控制。
7. IME preedit、外部显示所有权、不支持的收缩、超长硬行、过期
generation 或发布错误都会 fail closed 到原生显示。半成品投影会
回滚,原始错误继续向外报告。
原生软折行是状态机的前提,不能只希望用户碰巧开启它。mode 启用时先
保存 `truncate-lines``truncate-partial-width-windows` 的值与
buffer-local 所有权,再把二者设为 buffer-local `nil`;teardown 时恢复
原局部值,或移除临时局部绑定让全局值重新接管。这样 Emacs 默认的
50 列分栏阈值就不会悄悄把窄分栏变成横向滚动。
这个状态模型不需要 live lookahead、push/pull 收敛、逐键整行规划器或
idle formatter。point 移动本身严格零副作用;之后若在别处发生真实编辑,
则允许提交先前活动硬行,即使 narrowing 让该行已不在可访问区内。
不存在编辑空闲后整段 formatter。resize/后台工作都带 generation。
大 buffer 按可见优先的硬段落 chunk 处理;单个硬段落超过
`ekp-auto-justify-paragraph-limit` 时,自动路径保持自然显示,只有显式
`ekp-refill-paragraph` 才执行无界完整质量 pass。文本属性属于 buffer,
所以以最窄活动窗口作为唯一权威宽度。
### 5.1 断行许可、对齐、悬挂、段形
- **断行许可**:每个 CJK 字符(含标点)独立成盒;
`ekp-para-breaks-allowed` 按禁则(全角与半角)、`ekp-no-break`
区间及 NBSP 族连接符禁止相应间隙,被禁间隙不携带 glue。DP 跳过
被禁候选但继续延伸行。最终遍中的合法欠宽候选获得有限 emergency
stretch 并按正常 badness/demerits 评分;若超宽候选将消灭最后活动
路径,TeX 风格 artificial demerits 以零增量代价保留该路径。C 侧接收
稀疏 `forbidden-positions` 向量。显式 hard atom 只禁止其区间内部
断行;atom 前后本来合法的边界仍然合法。atom 邻接不获得特殊评分,
也不会额外禁止断点。
- **可配置 policy 编译**:buffer 与 core policy 变量在 tokenization
之前解析为私有结构区间。区域 `ekp-break-policy` 优先,显式
buffer/file/dir local 值优先于 mode profile,profile 优先于全局默认。
只有 token policy 是按类别合并的 map;标量与 face 列表都直接替换
低优先级值。core 可以在 analysis copy 上使用
`ekp--face-break-policy`、`ekp--no-hyphen`、`ekp--literal-spacing`
等私有属性,但 cache key、plan string、box、rendered string 与
`ekp--last-para` 只能保留公开源属性。自动 face no-break 按连续的
私有 face-policy 区间整体测宽,超宽时降级为 no-hyphen;显式
`ekp-no-break` 永不降级。
- **行内字面空格**:face 派生的 `no-hyphen` 在行内保留源空格为字面
box,禁止会把源空格 box 移到行首的断点,并允许互补的"空格后到内容"
断点。当这些空白成为选中的视觉断点时,源归属属于 break gap metadata,
而不是尾随可见行 box。该规则来自 policy,不能依赖源上是否另有公开
`face` 属性。
- **对齐**(`ekp-alignment`):非两端对齐把 glue 伸缩数组与类参数
置零,DP 给 `max_w` 加每行额外伸展 R(`ekp-c-set-penalties` 第 7
参),badness = 100·(欠宽/R)³;渲染层按模式分派剩余(尾部/对半/
头部)。
- **悬挂**(`ekp-protrusion`):逐间隙 `tail-protrudes[k]`(穿透尾
随空格盒取最后内容盒)加 `hyphen-protrude` 标量,在 DP、
`ekp-line-glues`、C 结果重建三处同步放宽每个候选的有效目标宽
(`lw = width + release`)——三处必须保持一致。
- **每行宽度**(`ekp-parshape` / `ekp-first-line-indent`):由
`ekp--line-spec`(行号 → 缩进 . 宽度)解析。纯首行缩进只改第 0 行,
而"以盒 0 开头的行"恰好对应 DP 起点 i = 0,故 1D 遍(以及 C 引擎,
`FIRST-LINE-WIDTH` 参数)无需额外状态即可处理;只有完整的
`ekp-parshape``ekp-looseness` 才需要(位置×行数)DP 并旁路 C。
缩进渲染为行首 `ekp-glue` 垫片。
C 模块 1.6:`ekp-c-break-with-arrays` 15 参(…、forbidden-positions、
tail-protrudes、hyphen-protrude、first-line-width);batch 向量 15 元;
`ekp-c-set-penalties` 48 参。policy 编译只进入现有断词位置与禁断
向量;没有新的架构决策时,不得增加第 16 个 C 参数或 batch 字段。
特性完成后的性能(字节编译 + C,Apple Silicon,batch):justify zh
w=200 ≈ 54 ms、range zh ≈ 117 ms——justify 与特性前持平,range 因盒
数增加约 +55%。热路径缓存:`ekp--str-type` 按字符记忆化、glue 字符
串驻留、(段落, 宽度) 渲染结果缓存进 dp-cache(上限 64 个宽度)。连
续变宽实测(60 段 2.6 万字文章,含 region 层全链路):每次变宽约
73 ms,重访宽度更快;编辑后单段增量重排约 17 ms。
`ekp-bench-adversarial-builders` 单独测量源码解释模式下的退化
builder。输入从 1,000 增到 8,000 字符时,基于片段的 tokenizer 增长
6.3×,密集插入增长 7.7×,接近输入 8× 的线性增长。8,000 字符分别从
3.133 s 降到 1.100 s、从 0.945 s 降到 0.013 s。断词位置缓存使用
显式 miss sentinel,因此合法的 nil 结果也能复用。
## 6. Looseness ## 6. Looseness
@ -323,65 +142,34 @@ builder。输入从 1,000 增到 8,000 字符时,基于片段的 tokenizer 增
## 7. C 模块集成 ## 7. C 模块集成
C 模块(`ekp_c/`,版本 1.6)只执行阶段 ④。所有字体相关数据以 Elisp C 模块(`ekp_c/`,版本 1.1)只执行阶段 ④。所有字体相关数据以 Elisp
为唯一事实来源。 为唯一事实来源。
- `ekp-c-break-with-arrays`(15 参数):para 的前缀数组、glue 数组、 - `ekp-c-break-with-arrays`(11 参数):para 的前缀数组、glue 数组、
断词数据、行宽、两个空格串数组、禁则/悬挂数组和首行宽度。返回 断词数据、行宽和两个空格串数组。返回 `(breaks . cost)`
`(breaks . cost)` - `ekp-c-break-batch`:11 元素向量的向量,由 pthread 线程池并行处理
- `ekp-c-break-batch`:15 元素向量的向量,由 pthread 线程池并行处理 ——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。
——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。线程池在 - `ekp-c-set-penalties`(46 参数):`ekp--c-sync-params` 在**每次**
首次多段落 batch 时惰性创建,按机器核心数定大小;队列满时提交方
阻塞等待而非丢弃任务。
- `ekp-c-set-penalties`(48 参数):`ekp--c-sync-params` 在**每次**
进入 C 之前调用,保证 `ekp-line-penalty` 等变量始终生效(回归:此 进入 C 之前调用,保证 `ekp-line-penalty` 等变量始终生效(回归:此
前从未同步)。 前从未同步)。
- `ekp-c-module-load` 拒绝低于 `ekp-c-module-required-version` 的模块 - `ekp-c-module-load` 拒绝低于 `ekp-c-module-required-version` 的模块
并回落到 Elisp,避免升级后的参数数量不匹配。 并回落到 Elisp,避免升级后的参数数量不匹配。
模块不可用、ABI 版本不兼容、整批 C 结果为 nil 或单项/断点结果为 C 端任何失败(返回 NULL)都会静默回落到 Elisp 引擎。两引擎输出逐字
nil 时回落到 Elisp。直接 API 的非法输入 signal 节一致,由 `ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 验证。
`ekp-c-invalid-input`。任何非 nil 的畸形后端结果 signal
`ekp-backend-contract-error`:结果 cons 形状错误、breaks 不是 list、
break 非整数/越界/非递增/未覆盖段尾、cost 非数字,或 batch 结果形状
错误。已启用后端发出的任何 signal 都会穿过公共 formatter,dispatcher
不捕获或隐藏。模块不会在部分失败时静默产出不同的排版。两引擎输出
逐字节一致,由
`ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 及 300 例性质
fuzz 验证。
### 未来方向:段落句柄 API `ekp-c-break-lines`(经 `ekp_paragraph.c`、`ekp_hyphen.c` 的 C 端自行
分词路径)是实验性的独立路径,ekp.el 不使用;见 `ekp_c/README.md`
每次 `ekp-c-break-with-arrays` 调用都会重新编组段落的宽度无关数组
(约 18·n 次 `env` 提取)。单次 justify 时这无关紧要,但
`ekp-pixel-range-justify` 会对每个候选宽度重新编组同一批数组:即便
段落缓存已暖,C 路径每个宽度仍约 12 ms,其中大部分是编组而非 DP
(整篇样本的 DP 约 2.5 ms)。
解法是 `make_user_ptr` 句柄:`ekp-c-para-upload` 把数组一次性拷进 C
结构体并返回带 GC finalizer 的句柄,`ekp-c-break (handle, width)` 之后
只传两个宽度相关标量。它**有意**不纳入本次发布——这是引入 C 端对象
生命周期的破坏性(2.0)ABI 变更,而常见交互路径(单次 justify、
`ekp-auto-justify-mode`)本就命中 dp-cache、避开了重复编组。当宽度
搜索或超大批处理成为瓶颈时,这是明确的下一步。
## 8. 断词(ekp-hyphen.el) ## 8. 断词(ekp-hyphen.el)
普通 Liang 模式算法: Liang 模式算法,兼容 Pyphen:
- `dictionaries/hyph_*.dic` 首次使用时编译为模式哈希并按路径缓存。 - `dictionaries/hyph_*.dic` 首次使用时编译为模式哈希并按路径缓存。
文件可为 UTF-8 或 ISO-8859(Emacs 自动检测;由 文件可为 UTF-8 或 ISO-8859(Emacs 自动检测;由
`ekp-test-hyphen-de-iso8859-dict` 验证)。 `ekp-test-hyphen-de-iso8859-dict` 验证)。
- `ekp-hyphen-create LANG` 先精确匹配,再逐级缩短(`"de_CH" → "de"`)。 - `ekp-hyphen-create LANG` 先精确匹配,再逐级缩短(`"de_CH" → "de"`)。
- 断点两侧默认至少保留 2 个字符。 - 断点两侧默认至少保留 2 个字符。
- 非注释 pattern 出现斜杠时失败关闭。libhyphen 替换规则只在断点
胜出时改变可见文字与宽度,当前固定宽度 box 无法诚实表达;编译器
计数后 signal `ekp-hyphen-unsupported-pattern`,公共排版入口保留该
错误。
- `dictionaries/MANIFEST.tsv` 将 49 个条目固定到 LibreOffice 提交
(另明确标记一个 legacy Basque 字节),记录 SHA-256、语法标记和许可
证据。`tests/check-dictionaries.sh` 做离线门禁,
`dictionaries/update.sh check` 在 macOS/Linux 对照固定上游字节。
词盒按 `^[左标点]* (拉丁词) [右标点]*$` 匹配,因此被标点包裹的词 词盒按 `^[左标点]* (拉丁词) [右标点]*$` 匹配,因此被标点包裹的词
(`(word)`、`word!`、`»word«`)仍可断词;标点粘在首/末音节盒上。 (`(word)`、`word!`、`»word«`)仍可断词;标点粘在首/末音节盒上。
@ -389,31 +177,12 @@ fuzz 验证。
## 9. 测试与基准 ## 9. 测试与基准
```bash ```bash
tests/run-tests.sh [emacs] # batch 可跑的 ERT 测试集 tests/run-tests.sh [emacs] # 36 个 ERT 测试,batch 可跑
tests/run-tests.sh [emacs] --random-order
tests/run-tests-isolated.sh [emacs] # 每个 ERT 使用全新进程
tests/check-dictionaries.sh # 离线清单/校验值门禁
dictionaries/update.sh check # 核对固定上游字节
make -C ekp_c PROFILE=portable # 默认可移植发布构建
make -C ekp_c PROFILE=native # 仅本机基准
make -C ekp_c PROFILE=debug # 调试符号,不优化
make -C ekp_c PROFILE=sanitize # ASan + UBSan
emacs -Q --batch -L . --eval '(setq ekp-use-c-module nil)' -l tests/ekp-bench.el emacs -Q --batch -L . --eval '(setq ekp-use-c-module nil)' -l tests/ekp-bench.el
emacs -Q --batch -L . --eval '(progn (require (quote ekp)) (ekp-c-module-load))' \ emacs -Q --batch -L . --eval '(progn (require (quote ekp)) (ekp-c-module-load))' \
-l tests/ekp-bench.el -l tests/ekp-bench.el
``` ```
可用 `EKP_TEST_SEED` 复现或改变乱序。测试 fixture 会动态恢复其隔离的
全部 EKP 配置;分派类测试必须经过公开排版入口,不能只断言内部资格
谓词。
GUI 矩阵需显式加载 `tests/ekp-gui-verify.el`。任一行失败时,它先打印
完整表格,再以状态码 1 退出ERT 套件包含该边界的强制失败负控。
`M-x ekp-c-module-build` 使用同一组四种 profile并在 `ekp_c/` 中以
argv 直接启动 make不再构造 shell `cd` 命令。发布/CI 使用
`portable``native` 仅用于将在同一机器运行的基准。
核心被测不变式:渲染行宽 == 目标宽(像素级对齐)、任意宽度下不丢内 核心被测不变式:渲染行宽 == 目标宽(像素级对齐)、任意宽度下不丢内
容、O(1) 前缀机制与暴力算法交叉验证、内置文本上的 Elisp/C 一致性、 容、O(1) 前缀机制与暴力算法交叉验证、内置文本上的 Elisp/C 一致性、
参数持久化/同步回归。 参数持久化/同步回归。
@ -447,11 +216,9 @@ ekp.el 核心:para 结构、缓存、DP(1D + looseness)、
ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体 ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体
检测、C 模块加载 检测、C 模块加载
ekp-hyphen.el Liang 断词 + 词典注册 ekp-hyphen.el Liang 断词 + 词典注册
ekp-buffer.el 纯文本属性 buffer/region 投影、同步实时流动、窗口
lifecycle、复制过滤与诊断
ekp_c/ C 动态模块(见 ekp_c/README.md) ekp_c/ C 动态模块(见 ekp_c/README.md)
dictionaries/ Hunspell 断词模式(来自 LibreOffice) dictionaries/ Hunspell 断词模式(来自 Pyphen)
tests/ ekp-tests.el、ekp-buffer-tests.el(ERT) tests/ ekp-tests.el(ERT)、ekp-bench.el、ekp-demo.el
ekp-fuzz.el(一致性 fuzz)、ekp-bench.el、 示例文本、run-tests.sh
ekp-demo.el、ekp-showcase.el、示例文本、run-tests.sh archive/ 历史原型;不参与加载,仅作参考
``` ```

View File

@ -1,56 +0,0 @@
# Releasing emacs-kp
Releases are immutable snapshots. Changed bytes require a new version,
new checksums, and a new tag; do not replace an existing release asset.
## Repository gate
Run the local invariant check before creating a release commit:
```sh
tests/check-release.sh
```
It verifies:
- every GitHub Action is pinned to a full commit SHA;
- Windows and the public ERT batch entry point are present in CI;
- `.phrase` decision records are not ignored;
- the package header has a matching dated changelog release;
- the Elisp C-module requirement matches the C header ABI version.
This static gate does not prove a remote workflow or published artifact.
## Release checklist
1. Move all shipped entries out of `Unreleased` into a dated
`[X.Y.Z] - YYYY-MM-DD` section and set `Version: X.Y.Z` in `ekp.el`.
2. If the C API changed, bump both `ekp-c-module-required-version` in
`ekp-utils.el` and `EKP_VERSION_MAJOR`/`EKP_VERSION_MINOR` in
`ekp_c/ekp_module.h`. Update the C version in user and developer docs.
3. Run:
```sh
tests/check-release.sh
tests/run-tests.sh emacs
EKP_TEST_SEED=20260728 tests/run-tests.sh emacs --random-order
tests/run-tests-isolated.sh emacs
tests/check-dictionaries.sh
dictionaries/update.sh check
make -C ekp_c PROFILE=portable
emacs -Q --batch -L . -l tests/ekp-fuzz.el
```
4. Byte-compile with warnings as errors and run package-lint/checkdoc using
the same pinned inputs as CI.
5. Verify the worktree is clean and the release commit is the reviewed commit.
6. Create `vX.Y.Z` at that exact commit. Never move an existing release tag.
7. Let the pinned Linux, macOS, Windows, and sanitizer CI jobs finish
successfully on the release commit.
8. Build release artifacts from the tagged commit with `PROFILE=portable`.
Record a SHA-256 checksum for every artifact.
9. Push the commit and tag, publish the immutable assets and checksums, then
verify the remote tag resolves to the reviewed release commit.
If any item fails, fix it in a new commit and restart from the repository
gate. Do not publish a partial release.

View File

@ -1,570 +0,0 @@
# emacs-kp 仓库系统审计
> 审计日期2026-07-28
> 审计基线:`main@29cef97`(本地 tag `v1.0.0`
> 范围Elisp 核心、C 动态模块、buffer/region 集成、测试、性能、兼容性、文档、发布与扩展边界
> 性质:问题盘点与方向判断,不包含运行代码修复
## 后续状态
- `task002` 已在审计后的工作树中解决 P1-01DP key 现在包含行宽、
looseness 与全部六个运行时代价参数,并按结构相等比较。
- `task003` 已继续解决 P2-01auto-CWS 默认值现在同时进入段落哈希键
与最近段落快路径;`issue001` 的两个缓存缺陷均已闭环。
- `task004` 已解决 P1-02保存从逻辑副本序列化文件系统错误、编码
错误与用户中断均不再改变显示 buffer`issue002` 已闭环。
- `task005` 已解决 P1-03/P2-06copy filter 可组合并恢复integration
生命周期由 auto mode 与实际 justified span 共同决定;`issue003`
已闭环。
- `task006` 已解决 P1-04测试 fixture 完整隔离配置parshape 用例
经过公开分派路径CI、可复现乱序和逐例全新进程入口共同阻断顺序
假绿,`issue004` 已闭环。
- `task007` 已解决 P2-05GUI 矩阵共享结构化结果与报告断言边界,
任一失败行在 batch 中退出 1强制失败负控 2/2真实 GUI 七场景
7/7`issue007` 已闭环。
- `task008` 已解决 P2-02C 1.6 在提取前验证单段/批量完整 schema
非法调用统一 signal `ekp-c-invalid-input`,有效 int32 输入使用
int64 中间量;模块 signal 不再被 Elisp 静默吞掉。
- `task009` 已解决 P2-03默认构建改为 portablenative/debug/
sanitize 需显式选择;交互命令以 argv 和 `default-directory` 启动
make含空格路径实测通过。`issue005` 已闭环。
- `task010` 已解决 P2-07 的仓库内边界CI action 与 package-lint
固定到完整提交Windows 运行 Elisp 基线,`.phrase` 改为可追踪事实
来源release gate 与远端/产物 checklist 分责。未执行 push、tag
变更或发布。
- `task011` 已解决 P2-0449 份词典固定来源路径与 SHA-256更新器
可在 macOS/Linux 重建相同输出4 份含斜杠/替换语法的词典明确失败
关闭,不再静默降级。缺少准确许可证据的 Sanskrit 文件已移除。
- `task012` 已按 benchmark 修复 nil 缓存与长文本构造热点;
`task013` 已补齐交互保护命令、反馈与 session-local 文档;
`task014` 已集中行边空格与 render-marker 规则归属。
- 最终独立审查新增的 P1-05 已由 `task016` 解决most-recent fast
path 与 hash cache 共享完整 `ekp--para-key`,同一字符串对象原地
修改排版属性也会失效;`issue010` 已闭环。
- `task015` 已完成终局清理与门禁:删除九个死 accessor 和依赖私有
substring-filter API 的调用;最终独立代码审查 `APPROVE`、架构审查
`CLEAR`,全部仓库内 issue/task 均已闭环。
- `task017` 已将当前编辑器集成模块直接改名为 `ekp-buffer.el`
下文的 `ekp-region.el``ekp-region-*` 仍保留为审计基线的历史
证据,不代表当前加载入口。
- 下文保留 `main@29cef97` 的原始证据与判断,不把历史问题改写成从未发生。
## 结论先行
emacs-kp 的算法地基是可靠的Elisp 掌握文本与字体事实C 只镜像动态规划热路径;严格遍/紧急遍保证困难输入不丢内容buffer 层用可逆布局标记保存逻辑文本测试覆盖了核心算法、C/Elisp parity、编辑器协议与真实 GUI 像素贴合。
审计基线最需要处理的不是重写算法,而是四个已经实证的边界缺陷:
1. 算法 penalty 改变后,既有 DP 缓存仍按旧参数返回结果。
2. 保存失败会让原本已排版的 buffer 留在未排版状态。
3. EKP 直接覆盖已有的 `filter-buffer-substring-function`,可能破坏其他 mode 的复制语义。
4. 一个 C 绕过测试依赖前序测试泄漏的全局状态,整套测试为绿但单独运行失败。
**【Taste Score】** 🟡 Acceptable
设计方向正确,关键算法有真实测试;但缓存键、保存事务和编辑器协议组合这几个边界还没有单一 owner。继续堆 fallback、watcher 或 hook 会放大问题,下一步应先修正状态模型与边界契约。
**【Fatal Issues】**
- `ekp--dp-key` 没有覆盖所有影响 demerits 的参数,配置修改会静默失效。
- save/copy 集成不是可组合、可回滚的事务边界。
- 测试 fixture 没有恢复完整的全局配置,已经产生假绿。
**【Improvement Direction】**
- 先把“影响缓存结果的完整输入”变成显式键。
- 把保存、复制、isearch 视为编辑器协议边界,做可组合和失败恢复。
- 让每个测试独立运行,并让 GUI/本地 C 边界失败能可靠返回非零。
- C 模块继续只做 DP除非 benchmark 证明跨边界复制是主要瓶颈,不要把字体、分词或渲染语义下沉到 C。
## 1. 审计方法与验证基线
### 1.1 读取范围
- 核心:`ekp.el`、`ekp-utils.el`、`ekp-hyphen.el`
- 编辑器集成:`ekp-region.el`
- C 模块:`ekp_c/*.c`、`ekp_c/*.h`、`ekp_c/Makefile`
- 测试与演示:`tests/*.el`、`tests/run-tests.sh`
- 文档与发布README、DEVELOPER、CHANGELOG、CONTRIBUTING、CI、词典更新脚本、`.phrase`
- 历史决策:`.phrase/phases/phase-kp-overhaul-20260726/HANDOFF.md`、`.phrase/phases/phase-p1p2-20260726/NOTES.md`
### 1.2 新鲜验证
| 验证 | 结果 | 说明 |
|---|---|---|
| 默认 ERT 入口 | PASS94/94 | Emacs 30.2,加载 C 1.5 |
| C/Elisp fuzz | PASS300/300 | 字节级一致0 failure |
| 临时干净副本 byte-compile | PASS | `byte-compile-error-on-warn=t` |
| 临时干净副本 checkdoc | PASS | 四个发布 Elisp 文件无输出 |
| 临时干净副本 C release build | PASS | C11、`-Wall -Wextra -Wpedantic` 无警告 |
| 临时干净副本 ERT + fuzz | PASS | 新构建 C 模块94/94 + 300/300 |
| GUI 像素矩阵 | PASS7/7 | base、缩放、face remap、无 fringe、窄窗均无 overflow |
| 单测独立负控 | FAIL符合审计预期 | `ekp-test-parshape-bypasses-c` 单独加载 C 后失败,确认测试顺序依赖 |
GUI 数值矩阵的七种场景均满足 `widest == target``over=0`。干净截图的第二次抓取被 macOS 录屏权限提示覆盖;未批准系统权限,因此矩阵输出可作为数值证据,但本次不把截图当成无噪声视觉证据。
### 1.3 未验证项
- 未运行本地 `package-lint`;仓库 CI 会从 MELPA 动态安装它,但本机没有固定版本。
- 未运行 Windows 构建与 GUI 验证。
- 未验证远端 GitHub Actions 的运行结果;本地分支尚未推到 `origin/main`
- C 极值整数风险未在 macOS sanitizer 下动态复现:`make DEBUG=1` 能构建,但当前系统因 sanitizer runtime code-signing policy 拒绝加载该模块。
## 2. 当前架构
```text
字符串 API
ekp-pixel-justify / ekp-pixel-range-justify
├─ ekp-utils分箱、字体与像素测量
├─ ekp-hyphenLiang pattern 编译与断词
├─ ekp.elekp-para、前缀数组、缓存
├─ DP
│ ├─ Elisp 1D / 2D
│ └─ C 1D15 字段边界,批量按段落并行)
└─ rendererglue、soft break、soft hyphen、hidden payload
buffer API
ekp-region.el
├─ justify / unjustify
├─ auto mode宽度跟随、dirty 段落、lazy chunk
└─ 编辑器协议save、isearch、kill/yank、undo、modified state
```
### 2.1 扎实的部分
1. **Elisp 是语义事实源C 只做纯 DP。**
证据:`ekp.el:1733-1749` 构造 15 字段输入;`ekp_c/ekp.c:590-611` 暴露对应 API`ekp_c/README.md` 明确字体相关数据留在 Elisp。
判断:这是正确边界。不要重新把分词、字体测量或渲染下沉到 C。
2. **严格遍 → 紧急遍的失败模型一致。**
证据:`ekp.el:1173-1202` 与 `ekp_c/ekp_kp.c:501-537`fuzz 300/300。
判断:困难输入不丢内容的核心承诺有代码与性质测试双重保护。
3. **布局是可逆结构,不是破坏性格式化。**
证据:`ekp.el:2180-2191` 定义四类布局标记;`ekp-region.el:402-445` 做结构反转region 测试覆盖 roundtrip、save、isearch、kill、undo。
判断:模型有价值,但所有编辑器协议都必须显式适配,生命周期边界必须更硬。
4. **CI 覆盖面优于一般小型 Elisp 包。**
证据:`.github/workflows/ci.yml` 覆盖 Emacs 29.1/30.1/snapshot、C parity、fuzz、ASan/UBSan、macOS。
判断测试类型正确主要缺口是可复现性、Windows 和失败退出契约。
## 3. 按优先级排序的问题
优先级含义:
- P0数据丢失、安全或默认路径不可用立即阻断发布
- P1已实证的核心行为错误或测试可信度缺陷
- P2重要边界风险、兼容性/维护性缺陷或已知能力落差
- P3优化与体验机会应由基准或真实需求驱动
本次未发现 P0。
### P1-01 算法参数不在 DP 缓存键中,修改后静默复用旧结果
- **Evidence**
- `ekp--dp-key` 只包含 `line-pixel` 和非零 `ekp-looseness``ekp.el:1184-1190`。
- demerits 直接读取 `ekp-line-penalty`、`ekp-hyphen-penalty`、`ekp-adjacent-fitness-penalty`、`ekp-consecutive-hyphen-penalty` 等全局值:`ekp.el:1080-1125`。
- README 声称所有算法参数对两个引擎生效。
- 实测同一段落/宽度先用 hyphen penalty 0再改为 1000000缓存对象仍相同清缓存后断点改变cost 从 `100226389.0` 变为 `601214639.0`
- **Inference**
- 问题 owner 是 DP 缓存键,不是 C 参数同步。C 同步只在真正计算时运行,命中缓存时根本不会进入同步或 DP。
- **Impact**
- 用户通过 `setq` 或 Custom 调整排版质量参数时,界面可能看起来“设置无效”,直到显式 `ekp-clear-caches` 或段落键变化。
- **Direction**
- 把所有影响 DP 结果的参数收敛到一个不可变的 DP key不要再靠零散 watcher 猜测失效点。
- 回归测试必须在不清缓存的情况下修改每一类参数,并断言结果或 cost 与 fresh computation 一致。
- **Confidence**High
### P1-02 保存失败会留下未排版 buffer 和 stale save state
> 后续状态2026-07-28已由 `task004` 修复。保存不再执行
> unjustify/rejustify 事务,而是在 `write-region` annotation 边界写入
> 隐藏逻辑副本;成功、文件系统失败、编码失败和 `quit` 均有回归覆盖。
- **Evidence**
- `before-save-hook` 先把全部 justified spans 反转:`ekp-region.el:461-479`。
- 只有 `after-save-hook` 会重排并清空 `ekp-region--save-state``ekp-region.el:481-492`。
- 负路径实测:让 visited file 的目标目录消失后调用 `save-buffer`,得到 `still-justified=nil`、`save-state=t`。
- **Inference**
- 成功路径测试不足以证明保存事务;底层写入报错或保存被中断时,`after-save-hook` 不会提供 finally 语义。
- **Impact**
- README 的“保存逻辑文本,同时屏幕保持排版态”承诺在失败路径不成立;后续保存还可能携带陈旧 marker。
- **Direction**
- 先定义保存事务的 owner 和失败恢复路径,再实现;恢复必须无条件执行,写盘失败仍要回到原显示态。
- 增加写盘失败、编码失败、用户中断三类负路径测试。
- **Confidence**High
### P1-03 覆盖已有 copy filter破坏其他 mode/user 的复制语义
> 后续状态2026-07-28已由 `task005` 修复。测试覆盖 local/global
> prior filter、复制、DELETE kill、手动 unjustify 与 mode 关闭。
- **Evidence**
- 安装集成时直接 `setq-local filter-buffer-substring-function``ekp-region.el:378-389`。
- 移除时只删除 EKP 自己的当前值,没有保存或恢复前一个 filter`ekp-region.el:391-399`。
- 负路径实测:预设 filter 为复制内容加 `PRE:`justify 后该前缀消失。
- **Inference**
- `filter-buffer-substring-function` 是单槽协议EKP 必须显式组合或恢复前一个 owner直接覆盖不是可组合集成。
- **Impact**
- 在已有复制过滤逻辑的 major mode 中kill/copy 可能丢失 mode 定义的语义。
- **Direction**
- 记录前一个 buffer-local filter明确调用顺序并在最后一个 justified span 消失时恢复。
- 测试已有 filter、EKP filter、删除式 kill 三条真实 public path。
- **Confidence**High
### P1-04 测试顺序依赖制造假绿
> **Resolved by task006 (2026-07-28):** fixture 使用动态绑定恢复全部
> tunableparshape 用例绑定真实 `ekp-parshape` 并驱动公开 formatter。
> 108/108 通过可复现乱序,全部 108 个测试也逐个在全新 Emacs 进程
> 中通过。
- **Evidence**
- `ekp-test-parshape-bypasses-c` 的名称和 docstring 测 parshape代码却绑定 `ekp-first-line-indent``tests/ekp-tests.el:225-228`。
- C 1.5 已支持 first-line indent真正绕过 C 的条件是 `ekp-parshape` 非 nil`ekp.el:1667-1675`。
- `ekp-test-params-affect-c-module` 在结束时把全局 `ekp-use-c-module` 留为 nil`tests/ekp-tests.el:434-448`。
- 全套 94/94单独加载 C 后运行该测试则 1/1 FAIL。
- **Inference**
- 默认字母序恰好隐藏了错误测试;当前 fixture 只恢复部分 penalty没有恢复引擎和全部样式状态。
- **Impact**
- 测试套件可能继续掩盖其他全局状态泄漏,绿灯可信度下降。
- **Direction**
- 让 clean-state fixture 保存/恢复所有全局 tunable该测试绑定真实 `ekp-parshape`
- CI 增加 isolated 或 randomized-order lane至少覆盖依赖全局状态的测试。
- **Confidence**High
### P1-05 同一字符串对象的属性修改绕过完整段落 key
> 后续状态2026-07-28已由 `task016` 修复。most-recent slot
> 保存并比较完整 `ekp--para-key`,删除六个补偿性 style watcher
> CJK 与 Latin-with-space 原地属性修改回归、专项缓存矩阵及完整
> default/permuted/isolated ERT 均通过。
- **Evidence**
- `ekp--para-key` 包含过滤后的 text-property intervals。
- `ekp--get-para` 的 most-recent fast path 只比较对象 identity、语言、
width context 与 spacing signature。
- 独立审查实测warm `"文中"` 后在原对象加入 `ekp-no-break`,下一次
lookup 仍返回原 paragraph清缓存后的 fresh paragraph 不允许相同
break。
- **Inference**
- 这不是 hash collision而是第二套不完整 identity 规则绕开了唯一
完整 key。
- **Direction**
- 让 fast path 与 hash cache 共享 `ekp--para-key`,删除并行维护的
partial signature/watchers。
- 用 CJK 与 Latin-with-space 两类原地 property mutation 锁定 public
paragraph resolver。
- **Confidence**High
### P2-01 自动 CJK stretch 默认值不在段落缓存键中
> 后续状态2026-07-28已由 `task003` 修复。新增回归分别覆盖
> 段落哈希键、`ekp--last-para` 快路径及相同签名真实命中;修复没有
> 新增 watcher。
- **Evidence**
- `ekp-default-cws-stretch-pixel` 定义于 `ekp.el:94-97`,在 auto 参数计算中使用:`ekp.el:329-336`。
- auto 模式的 para key 只写入符号 `auto``ekp.el:740-746`。
- 实测把默认值从 2 改为 9 后,返回同一个 para缓存中的 `:cws-stretch` 仍为 2。
- **Inference**
- 这是 P1-01 同一类模型缺陷:缓存键没有完整表达决定结果的输入。
- **Direction**
- 将该值纳入 auto 参数 snapshot/key测试修改后无需手动清缓存。
- **Confidence**High
### P2-02 C API 的输入契约、错误语义与数值范围不闭合
> **Resolved by task008 (2026-07-28):** C 1.6 对 15 字段 shape、长度、
> 类型和 int32 范围做整体验证,非法输入使用专用 conditionDP 中间
> 行宽/前缀差/剩余空间改为 int64。六项边界红测 0/6 → 6/6公开
> dispatcher error 传播 0/1 → 1/1完整 ERT 116/116、fuzz 300/300。
- **Evidence**
- 文档说 bad argument 返回 nil直接传入长度错误的 vector 实测得到 `args-out-of-range`
- `ekp_c/ekp.c:176-237`、`323-374` 在多次 `vec_get`/`extract_integer` 后才统一检查 pending non-local exit。
- 输入先 clamp 到 `int32_t`,随后在 `ekp_c/ekp_kp.c:287-304` 继续做有符号加减。
- **Inference**
- 正常 `ekp.el` 调用会捕获 signal 并回落 Elisp因此常规文本不受影响但直接 C API 的文档契约不真实。
- `INT32_MAX` 附近的 width/protrusion/prefix 组合可能触发 C signed overflow。真实字体不接近此范围但公开 API 没有限制它。
- **Direction**
- 先校验 15 字段 arity、`n/n+1` 数组长度、排序位置和数值范围,再分配/提取。
- 统一选择“明确 signal”或“返回 nil”不要文档与实现各说一套。
- 内部度量改为 `int64_t` 或 checked/saturating arithmeticAPI 边缘再收窄。
- 增加 malformed vectors、极值、batch >1024 的 C 边界测试。
- **Confidence**Medium-High
### P2-03 C 构建入口对路径、CPU 和调试环境过度假设
> **Resolved by task009 (2026-07-28):** `PROFILE=portable` 成为默认,
> native/debug/sanitize 独立;交互构建不经过 shell。两项红测
> 0/2 → 2/2四 profile 均零警告,真实交互构建与含空格副本构建
> 均成功。
- **Evidence**
- `ekp-c-module-build` 拼接 `cd %s && make` 交给 shell路径没有 quote`ekp-utils.el:405-419`。
- 默认 Makefile 使用 `-march=native -flto``ekp_c/Makefile:33-36`。
- README 把普通 `make` 描述为通用入口。
- 当前 macOS 上 `make DEBUG=1` 构建成功,但加载时因 sanitizer runtime code-signing policy 失败。
- **Inference**
- 安装路径含空格或 shell 元字符时,交互 build 会失败,特殊路径还形成命令注入面。
- `-march=native` 适合本机 benchmark不适合分发预编译产物。
- **Direction**
- 用进程 `default-directory` + 参数向量调用 `make`,不要启动 shell 执行 `cd`
- portable 作为默认 profile`NATIVE=1`、`LTO=1`、`DEBUG=1` 显式选择。
- 文档分别说明 Linux sanitizer 与 macOS 调试模块的 runtime/code-signing 条件。
- **Confidence**High
### P2-04 词典覆盖与 parser 能力被文档高估
> 后续状态2026-07-28已由 `task011` 闭环。进一步核对发现除
> 2399 条三语言 replacement rules 外Esperanto 还有 4 条斜杠模式。
> 当前固定宽度 DP 无法表达“仅断点胜出时替换文字与宽度”,因此四份
> 词典统一 signal 而非产生错误断词49 项 manifest/upstream gate 与
> 双导出一致性均通过。
- **Evidence**
- 仓库实际有 50 个 `hyph_*.dic`,不是 README 所写的 70+。
- `ekp-hyphen--compile` 跳过所有含 `/` 的 alternative patterns`ekp-hyphen.el:114-153`。
- 仓库中 3 个词典含 2399 条非注释 alternative pattern主要来自 Hungarian也涉及 Catalan/Albanian。
- 50 个词典中有 20 个没有同名 `README_hyph_*.txt`README 却要求“见每个词典的 README”。
- `dictionaries/update.sh` 拉取移动中的 LibreOffice HEAD没有 pinned commit、manifest、checksum、错误退出或跨平台处理。
- **Inference**
- “任意 bundled dictionary 都完整工作”不成立。普通 Liang patterns 可用,但 replacement/alternative 语义被有意忽略,相关语言的断词质量会下降。
- 许可证不一定不合规,但仅凭当前仓库无法完整核对每个词典的来源版本与许可文件。
- **Direction**
- 先诚实声明 parser 支持的语法子集。
- 为 Hungarian/Catalan/Albanian 加 golden words再决定是否值得实现 replacement semantics。
- 更新脚本必须 pin upstream commit生成来源/许可/checksum manifest并在 macOS/Linux 都可运行。
- **Confidence**High
### P2-05 GUI 验证工具有结果,但不是可靠的发布门禁
> **Resolved by task007 (2026-07-28):** 单场景返回结构化结果,统一
> report 在 batch 任一失败时退出 1。README 明确工具加载边界;强制
> 失败/成功负控 2/2真实 GUI 矩阵 7/7干净全屏单窗口截图已复核。
- **Evidence**
- README 推荐 `M-x ekp-gui-verify`,命令实际位于 `tests/ekp-gui-verify.el`,普通 `(require 'ekp-region)` 不会定义它。
- `ekp-gui-verify-matrix` 在 noninteractive 下只打印 `FAIL` 文本,不会以非零状态退出:`tests/ekp-gui-verify.el:184-191`。
- 本次真实 GUI 矩阵 7/7 PASS。
- **Inference**
- 工具对人工诊断有价值,但当前无法直接成为 CI gate用户也可能按 README 执行一个尚未加载的命令。
- **Direction**
- 二选一:把单次诊断纳入发布包,或明确给出加载 tests 工具的命令。
- matrix 发现任何 FAIL 时必须 signal/exit 1表格只是报告不是判定。
- **Confidence**High
### P2-06 手动 unjustify 后集成 hook/filter 残留
> 后续状态2026-07-28已由 `task005` 修复。auto mode 外最后
> justified span 消失时,公共 unjustify 与外部删除路径都会卸载
> integrations内部 reflow/isearch 继续使用不改变生命周期的核心反转。
- **Evidence**
- `ekp-unjustify-region` 只反转文本属性:`ekp-region.el:402-445`。
- integrations 只在 minor mode 关闭路径移除:`ekp-region.el:928-938`。
- 实测全 buffer unjustify 后已经没有 justified span但 save/isearch hooks 和 local copy filter 仍存在。
- **Inference**
- 无 justified span 时多数 hook 会空跑,但 copy filter 仍占据协议槽;它与 P1-03 共享生命周期 owner。
- **Direction**
- 由“buffer 是否仍有 justified span / auto mode 是否开启”派生 integration 生命周期,不要让手动命令和 mode 各自维护一套真相。
- **Confidence**High
### P2-07 发布与文档治理没有闭环
> 后续状态2026-07-28仓库内治理已由 `task010` 闭环。静态 gate
> 检查 action SHA、Windows/ERT 路径、`.phrase`、package/changelog
> 与 C ABI 版本;远端 CI、tag 和 content-addressed artifact 由发布
> checklist 在实际发版时验证,本轮没有冒充已发布。
- **Evidence**
- 本地 `main``origin/main` ahead 31本地 `v1.0.0` 指向 HEAD远端没有该 tag。
- `CHANGELOG.md` 与 package header 已声明 1.0.0。
- `.phrase/``.gitignore` 忽略,但仓库协议又把它定义为需求/task/change/issue 的事实源。
- 审计前 README 的 C 版本、词典数量、测试数量、GUI 工具入口均与实现不一致。
- CI 使用 `purcell/setup-emacs@master`、运行时最新 MELPA `package-lint`、`macos-latest`,没有 Windows job。
- **Inference**
- 如果 GitHub `origin` 是正式发布面,则 1.0.0 尚未真正发布;如果不是,仓库缺少明确的发布状态说明。
- 被忽略的 `.phrase` 可以做本地过程数据,但不能同时作为可审计的版本化事实源。
- **Direction**
- 在 push 前增加 release gatecommit/tag/changelog/version/CI/checksum 一致。
- 明确 `.phrase` 是 tracked source of truth 还是 local workflow state只能选一个。
- pin CI action/工具版本,增加 Windows 构建 lane。
- **Confidence**High
### P3-01 tokenizer 与断词缓存还有局部性能债
> **Resolved by task012 (2026-07-28):** nil cache 使用显式 miss
> sentineltokenizer 与密集插入改为片段单次拼接。8,000 字符耗时
> 分别从 3.133 s 降到 1.100 s、从 0.945 s 降到 0.013 s完整 ERT
> 124/124、fuzz 300/300、warning-as-error 编译与 checkdoc 通过。
- **Evidence**
- `ekp-split-to-boxes` 在字符循环中反复 `concat``ekp-utils.el:253-316`。
- `ekp-hyphen-inserted` 每个断点重建字符串:`ekp-hyphen.el:216-224`。
- `ekp-hyphen--positions``(or (gethash ...) (puthash ...))`;缓存值为 nil 时无法区分 miss。对无 pattern 的 `qzxq` 连续查询两次,`ekp-hyphen--compute` 实际调用两次。
- **Inference**
- 极长 URL、长 token、重复 acronym 或大量零宽字符下会出现额外分配;正常段落不是当前最大瓶颈。
- **Direction**
- 先把 benchmark 加入这些退化输入,再做线性 builder 与显式 cache sentinel。
- **Confidence**High
### P3-02 核心函数与 `ekp-para` 承担了过多规则
> **Resolved by task014 (2026-07-28):** 既有 `ekp--dp-key` /
> `ekp--line-ideal-pixel` owner 保留;四处重复的行缘空格公式收敛到
> 一个内联纯规则renderer/region 的五项 marker 词汇表收敛到一个
> 常量。直接规则测试与既有 roundtrip/C parity 共同锁定边界,未拆
> 文件、未新增热循环分配。
- **Evidence**
- `ekp-para` 同时保存分箱、测宽、glue、禁则、悬挂、offset 和 DP cache`ekp.el:215-246`。
- `ekp--make-para`、1D/2D DP、renderer 都是长流程space/protrusion 公式还必须在 DP、C 重建和 renderer 三处保持一致。
- **Inference**
- 继续加 typography 选项时,最可能出现的错误不是算法本身,而是忘记更新 key、15 字段边界或渲染反变换。
- **Direction**
- 不拆新文件。先在 `ekp.el` 内把“cache signature”“line metrics”“render marker protocol”变成三个可直接测试的规则 owner。
- 只抽取能消除重复规则的纯函数;不要建立 helper ladder。
- **Confidence**High
### P3-03 交互入口与临时标记的产品语义不够清楚
> **Resolved by task013 (2026-07-28):** 四个保护命令均通过真实
> interactive/public formatter 路径测试minor mode 增加标准 EKP
> 菜单与 help命令反馈及双语文档明确属性仅在当前 buffer session
> 有效。未引入持久化格式或全局快捷键。
- **Evidence**
- minor mode keymap 只 remap `fill-paragraph``ekp-region.el:897-899`。
- no-break/verbatim 命令只写入普通 text property`ekp-region.el:568-595`;普通文件保存重开不会保留这些属性。
- **Inference**
- 用户难以发现 justify/unjustify/protect 操作,也可能误以为手工保护会持久化。
- **Direction**
- 先补 mode help、状态反馈和“仅当前 buffer session 有效”的文档;只有真实用户需要时再设计持久化语法映射。
- **Confidence**Medium
## 4. 优化与扩展路线
### 4.1 第一优先级:恢复行为可信度
1. 完整 DP cache signature。
2. 保存失败的无条件恢复。
3. copy filter 组合与 integrations 单一生命周期。
4. 测试 fixture 隔离、错误 parshape 测试修正、负路径测试。
这四项完成前,不建议扩大 typography 功能面。
### 4.2 第二优先级:硬化 C 与发布边界
1. 15 字段 schema preflight、`int64_t`/checked arithmetic。
2. portable/native/debug 三种构建 profile。
3. 可诊断的 C fallback用户路径仍回落但 debug/CI 能看到失败原因。
4. GUI matrix 非零退出、Windows job、固定 CI 依赖。
5. 词典来源/许可/checksum manifest。
### 4.3 性能方向:只做有基准支撑的工作
| 方向 | 当前判断 | 启动条件 |
|---|---|---|
| tokenizer 线性 builder | 小而明确 | 长 token benchmark 显示明显非线性 |
| nil-aware hyphen cache | 低成本 | 可直接修复并加计数测试 |
| C paragraph-handle API | 潜在高收益、高复杂度 | range search profile 证明 15 数组重复 marshal 是主要瓶颈 |
| 更多线程内并行 | 不建议 | 单段 DP 有前向依赖,当前按段落并行边界正确 |
| 把测宽/分词移入 C | 不建议 | 会拆散字体事实源并放大 parity 成本 |
### 4.4 可选产品扩展
1. **精确 width search 模式**
当前 ternary + local scan 明确不保证全局最优。可为小范围提供 exhaustive 选项,并保留当前快速默认。
2. **按段落选择断词语言**
当前 `ekp-latin-lang` 是全局单值。若真实文档有多语言段落,可增加 buffer-local resolver不要先做自动语言识别。
3. **可发布的诊断入口**
`ekp-diagnose` 与 GUI fit check 收敛成一个用户入口:快速检查当前 buffer需要完整矩阵时再加载 tests 工具。
4. **mode-native verbatim/no-break**
Org/Markdown 优先从语法/face 派生保护,不把瞬时 text property 当持久存储。其他 mode 通过一个 buffer-local predicate 接入。
5. **发布包与词典可选化**
词典占仓库约 6.1 MiB。若分发场景确有需要可提供 core + language packs没有安装/更新痛点前不要拆包。
### 4.5 明确不做
- 左缘悬挂与行中 glyph advance 压缩:当前 Emacs 显示模型不能可靠表达,已有文档说明。
- 把一个 coherent `ekp.el` 按“utils/common”标签拆成多个文件。
- 为了“以后也许需要”而新增协议层、adapter 或兼容 shim。
- 在没有 profile 数据前重写 C DP 或增加候选级并行。
## 5. 建议的验收门槛
### 核心行为
- 所有影响 DP 的参数修改后,不清缓存也与 fresh computation 一致。
- 保存成功、保存失败、用户中断后buffer 的显示态、逻辑文本、modified state 和 marker state 都正确。
- 已有 copy filter 与 EKP filter 组合后,两方语义都保留。
### 测试
- 每个 ERT 可单独运行。
- randomized order 通过。
- 现有 94 ERT、300 fuzz、C parity 全通过。
- GUI matrix 任一 FAIL 返回非零。
### C 边界
- malformed vector 不越界、不继续带 pending exit 调用 API。
- 数值上限有文档、有检查、有 sanitizer 覆盖。
- portable build 不含 `-march=native`native benchmark profile 单独启用。
### 发布
- package version、C ABI version、README、CHANGELOG、tag、远端 commit 对齐。
- CI action/tool 版本固定。
- 词典 manifest 能回答“来源 commit、文件 checksum、许可文件”。
## 6. Evidence / Inference / Unknown 总结
### Evidence
- 默认与 fresh-build 测试均 94/94fuzz 300/300GUI 数值矩阵 7/7。
- 四个 P1 问题均有代码路径与负控/独立复现。
- README/C ABI/test count/词典数量与仓库事实曾发生漂移。
- 本地 main/tag 与 origin 状态不一致。
### Inference
- C `int32_t` 极值会造成 signed overflow 风险,但常规字体输入不会接近该范围。
- alternative patterns 被忽略会降低相关语言质量;具体单词影响需要 golden corpus 定量。
- 交互可发现性不足与 text property 非持久化会造成用户困惑,尚无用户研究数据。
### Unknown
- Windows C 模块和 region UI 在当前版本是否完整通过。
- GitHub Actions 在尚未推送的 31 个本地提交上是否通过。
- 远端缺少 v1.0.0 是有意暂缓还是发布遗漏。
- 用户是否真正需要精确 width search、多语言 resolver 或 language packs。
## 7. 停止条件
本次任务在以下状态停止:
- 已覆盖代码、C、测试、GUI、文档、发布与扩展面
- 已把问题按证据和优先级写入文档;
- 已给出优化方向与“不做什么”;
- 未修改运行代码,也未推送/发布;
- 具体修复应从 P1-01 开始,每次只闭环一个原子问题。

View File

@ -1,37 +0,0 @@
# Dictionary provenance and license evidence
The dictionary bytes in this directory are third-party data, not covered
solely by emacs-kp's `COPYING`. `MANIFEST.tsv` identifies the evidence for
every bundled file:
- `embedded` means the dictionary itself contains its copyright and license
or redistribution notice.
- `README_hyph_*.txt` names the checked-in companion notice.
- `LICENSES.md` refers to one of the pinned upstream companion files below.
The pinned LibreOffice snapshot is
`8fb8e794237cff49ec212023f96bcdb7d3fbf56c`. Immutable source links:
| Dictionaries | Upstream license evidence |
|---|---|
| `hyph_eo.dic` | [`eo/license-en.txt`](https://github.com/LibreOffice/dictionaries/blob/8fb8e794237cff49ec212023f96bcdb7d3fbf56c/eo/license-en.txt) |
| `hyph_id_ID.dic` | [`id/LICENSE-dict`](https://github.com/LibreOffice/dictionaries/blob/8fb8e794237cff49ec212023f96bcdb7d3fbf56c/id/LICENSE-dict) |
| `hyph_mr_IN.dic` | [`mr_IN/COPYING`](https://github.com/LibreOffice/dictionaries/blob/8fb8e794237cff49ec212023f96bcdb7d3fbf56c/mr_IN/COPYING) |
| `hyph_nl_NL.dic` | [`nl_NL/license_en_EN.txt`](https://github.com/LibreOffice/dictionaries/blob/8fb8e794237cff49ec212023f96bcdb7d3fbf56c/nl_NL/license_en_EN.txt) |
| `hyph_ru_RU.dic` | [`ru_RU/README_ru_RU.txt`](https://github.com/LibreOffice/dictionaries/blob/8fb8e794237cff49ec212023f96bcdb7d3fbf56c/ru_RU/README_ru_RU.txt) |
`hyph_eu.dic` is a legacy checked-in source whose byte is not present in the
pinned LibreOffice tree. Its origin, copyright, redistribution terms, and
checksum are retained in `README_hyph_eu.txt` and `MANIFEST.tsv`; the updater
copies that verified local byte instead of pretending it came from the
snapshot.
`hyph_sa_IN.dic` is intentionally not bundled. At the pinned snapshot it has
neither an in-file copyright/license notice nor a package companion that
states terms for the hyphenation data. LibreOffice reverted a generic
`COPYING` addition because spelling and hyphenation files may have different
licenses. Re-add Sanskrit only after an authoritative license statement for
that exact data is available.
License texts and notices remain the authority. This index is provenance
metadata, not a reinterpretation of their terms.

View File

@ -1,54 +0,0 @@
# emacs-kp dictionary manifest version 1
# upstream https://github.com/LibreOffice/dictionaries.git
# upstream-commit 8fb8e794237cff49ec212023f96bcdb7d3fbf56c
# normalization CRLF is converted to LF; hyph_sr-Latn.dic is renamed hyph_sr_Latn.dic
# columns file source-path sha256 license-evidence unsupported-slash-patterns
hyph_af_ZA.dic af_ZA/hyph_af_ZA.dic 541a45d73d92c69445613ec558c2c1a6829550d1a1ad8aa9dc6dfb752db87da1 embedded 0
hyph_as_IN.dic as_IN/hyph_as_IN.dic 321ffb692c12792dddb4294ab5857821fc1ab8f2d0a8c0fcdced00200602ead6 embedded 0
hyph_be_BY.dic be_BY/hyph_be_BY.dic e4339b5a53d2f0fb4179c1060173788d5b6ed3f07bbbccb8b74f5cea614a809e embedded 0
hyph_bg_BG.dic bg_BG/hyph_bg_BG.dic 7dad4f2ccfce1909b27f53082cf57c7914809a8ba31ccbcee3dc817dcd7c7994 README_hyph_bg_BG.txt 0
hyph_ca.dic ca/dictionaries/hyph_ca.dic 4a4816e93a92df35819c5f4775106577aa279739a12c687291d747a3ce767b8b embedded 2
hyph_cs_CZ.dic cs_CZ/hyph_cs_CZ.dic a378d024438b446ac0524237dc6d728dc520550db5fe0fb1fdbb47c2ee0ca525 README_hyph_cs_CZ.txt 0
hyph_da_DK.dic da_DK/hyph_da_DK.dic 44d0dd1d85bcdb1e63003c97cd08924f10a8a8127380b1b07ac2e75a950c216b README_hyph_da_DK.txt 0
hyph_de_AT.dic de/hyph_de_AT.dic 3e0b4687479d25099e37ebd946998c08fcccc12a0acf9380d8b19319a50b9f3a embedded 0
hyph_de_CH.dic de/hyph_de_CH.dic 825159436515ab150d3da0ddf5eedb814c0ec6c7eec98fbd53e3ba5b2a4a1443 embedded 0
hyph_de_DE.dic de/hyph_de_DE.dic 2e2f5ffea6bc53f67ffe8c076921c7b1b34a2ec2b9ec57a4608374bb65a1a1e1 embedded 0
hyph_el_GR.dic el_GR/hyph_el_GR.dic f2284153c3f9a809b959dcaeb349e00d90b4c4cf37c3d3a338b371c32f10e89d README_hyph_el_GR.txt 0
hyph_en_GB.dic en/hyph_en_GB.dic 9fdc97f7faabcda30c15a9d53bc7fbf21daec795fb3c9a9588f7bdb30c563f41 README_hyph_en_GB.txt 0
hyph_en_US.dic en/hyph_en_US.dic 486fb6840b1049d5d252ef784be11cd4dc2be0bf296568b74b41e4f4e17cba7d README_hyph_en_US.txt 0
hyph_eo.dic eo/hyph_eo.dic f2feb4cabe54b696affec72960b1670cfd9e4eae007854944bd02c0d348f59d7 LICENSES.md 4
hyph_es.dic es/hyph_es.dic a95be7365a482b930ade0ff54d413cd24357ade97cefd434b704286eda828d71 embedded 0
hyph_et_EE.dic et_EE/hyph_et_EE.dic 2ee13914fcd9e9b110e9e3d56e1d6acc28bf792c15b1b69efc2e71ac96c23c74 README_hyph_et_EE.txt 0
hyph_eu.dic legacy 5f499f513435a8432961750fae7b31bc5d3989b686a744c331bb75471a14b71b README_hyph_eu.txt 0
hyph_fr.dic fr_FR/hyph_fr.dic 476ca60b958400c8b3fbe55764b4900df87acf60394ef62362705d9c701db191 README_hyph_fr.txt 0
hyph_gl.dic gl/hyph_gl.dic 357355b82eb82ed081433503327ac4ba5b4f3606b6a3e50d69091d57eef4d5b8 README_hyph_gl.txt 0
hyph_hr_HR.dic hr_HR/hyph_hr_HR.dic 77613ff6e907d2e9ffd93d45bfb16873aa9182e2215df276dbf850d1b0e28f65 README_hyph_hr_HR.txt 0
hyph_hu_HU.dic hu_HU/hyph_hu_HU.dic 354939e518b838dbfe01ab617c25c94586b1accc2d5bdcf57ba07406e8d1fee9 embedded 2396
hyph_id_ID.dic id/hyph_id_ID.dic ac53214620527f8a145ce4d70f0a3a5d336a145643b52547375aedacef558dc0 LICENSES.md 0
hyph_is.dic is/hyph_is.dic 690ed04b72b60d05f66971ad9fd799656419228bfa8df00190adf02e27c0b1ef README_hyph_is.txt 0
hyph_it_IT.dic it_IT/hyph_it_IT.dic 8c25b6fb62c2fb97586c6f7c6fb48f29f49e90a5c33c09ac9112dba6942b50cd README_hyph_it_IT.txt 0
hyph_kn_IN.dic kn_IN/hyph_kn_IN.dic a15acb05a834979904330afda768accfa7fee10da386cc534d7944144eb1997f embedded 0
hyph_lt.dic lt_LT/hyph_lt.dic 376e9fbdc09db5feb0025181d84242b6e01e02f413301f7d059fd99dc0597316 README_hyph_lt_LT.txt 0
hyph_lv_LV.dic lv_LV/hyph_lv_LV.dic 775d9a539fd8e98fabbfefd87af5887bb24eb4700b47ed5f8e360b0bab75b078 README_hyph_lv_LV.txt 0
hyph_mn_MN.dic mn_MN/hyph_mn_MN.dic c49d5ead63948d84c75d43245d837a9d7c8dc36c2a97dc5e8a7ade031c7a00e9 embedded 0
hyph_mr_IN.dic mr_IN/hyph_mr_IN.dic 740707cd6d149d1339e921e126a25dcea889712f4889fbc72434d753324409ed LICENSES.md 0
hyph_nb_NO.dic no/hyph_nb_NO.dic c6c9b07d9f2c60bb47857fd629d9a453026312c894665d22c72e8e1699420f6c README_hyph_NO.txt 0
hyph_nl_NL.dic nl_NL/hyph_nl_NL.dic 0a97b76b259ed4165d094dedf9b6fb98c57734a437df0b5b263ee13e262d5f8f LICENSES.md 0
hyph_nn_NO.dic no/hyph_nn_NO.dic c6c9b07d9f2c60bb47857fd629d9a453026312c894665d22c72e8e1699420f6c README_hyph_NO.txt 0
hyph_or_IN.dic or_IN/hyph_or_IN.dic 264b3a880fc7dc9f477abac80708172144ec3bb28baa8266539fe7ffff7a8a30 embedded 0
hyph_pa_IN.dic pa_IN/hyph_pa_IN.dic 87b2e3d7dc8cc4e4f0bb1d6f4265e8d8eefa478fbc890a60839868af4f5c2c06 embedded 0
hyph_pl_PL.dic pl_PL/hyph_pl_PL.dic 9fe3917ec1fb09b7ffa560e85fb810e098d45a9a45ad6d5c08311544dd92a100 README_hyph_pl_PL.txt 0
hyph_pt_BR.dic pt_BR/hyph_pt_BR.dic a8eb1a64e4af4649c739211b397fa37b01f9d5774cf558fe3895fc3f26fd33a4 README_hyph_pt_BR.txt 0
hyph_pt_PT.dic pt_PT/hyph_pt_PT.dic 6f18ee89de6db21d9b06120a5527ec41d8132514fc112ccfe7d289ba91649840 README_hyph_pt_PT.txt 0
hyph_ro_RO.dic ro/hyph_ro_RO.dic 24adbfe7cc2267ea5ce4afd34f7d69da5a9163ef3dd57b158bbb196b9d651e94 embedded 0
hyph_ru_RU.dic ru_RU/hyph_ru_RU.dic e9a6ae92765fc7e63d879f2ccf3f50f2b95bc8ed7618a6f31c34ce7d39844c83 LICENSES.md 0
hyph_sk_SK.dic sk_SK/hyph_sk_SK.dic 7359863ec5d5dbbf35c7d4f38faca1af7b43bc4aa643871e63f2dc0dfac4f922 README_hyph_sk_SK.txt 0
hyph_sl_SI.dic sl_SI/hyph_sl_SI.dic 3d9026755aa0936e9331595a08bb65c295ccac84acefe58cb6c9fe2ff11ceef8 README_hyph_sl_SI.txt 0
hyph_sq_AL.dic sq_AL/hyph_sq_AL.dic 99fbce3b0e60cc500e45643dd40903dfa71cfc1605a5b139cfb22129bd665dbe README_hyph_sq_AL.txt 1
hyph_sr.dic sr/hyph_sr.dic ce785026592f525c38e67152699224d47a4cdf555a1a4eb65d50458c5548f69f README_hyph_sr.txt 0
hyph_sr_Latn.dic sr/hyph_sr-Latn.dic b423f910400472979a2b0fe1c95965c940e927f7588dace0e42c17f7b0663376 README_hyph_sr.txt 0
hyph_sv.dic sv_SE/hyph_sv.dic 44c5a59f1ef8a17b9433e4510a1abcac753d34ac875dc27ea95427f1e581f17b README_hyph_sv.txt 0
hyph_te_IN.dic te_IN/hyph_te_IN.dic 2ad470071dfd9bb3ad5101d528e1fb0090d96c841c4f2bb07b325476b3c853fe embedded 0
hyph_th_TH.dic th_TH/hyph_th_TH.dic fbd19d561c19a13d1901a0b9653e24b4c0c2c81177064266e2acb23bac68145b README_hyph_th_TH.txt 0
hyph_uk_UA.dic uk_UA/hyph_uk_UA.dic ae80645b757d6b57e5cdf34ab6cf7441a2414c60bff2352c7e815c1c12baad3d README_hyph_uk_UA.txt 0
hyph_zu_ZA.dic zu_ZA/hyph_zu_ZA.dic e75802e08127877d38891c140e8989af559665bae82f94d33c340cba2aa9de20 embedded 0
Can't render this file because it has a wrong number of fields in line 2.

116
dictionaries/hyph_sa_IN.dic Normal file
View File

@ -0,0 +1,116 @@
UTF-8
LEFTHYPHENMIN 3
RIGHTHYPHENMIN 4
COMPOUNDLEFTHYPHENMIN 2
COMPOUNDRIGHTHYPHENMIN 3
% GENERAL RULE
% Do not break either side of ZERO-WIDTH JOINER (U+200D)
22
% Break after ZERO-WIDTH NON JOINER (U+200C)
1
% Break before or after any independent vowel.
1अ1
1आ1
1इ1
1ई1
1उ1
1ऊ1
1ऋ1
1ॠ1
1ऌ1
1ॡ1
1ए1
1ऐ1
1ओ1
1औ1
% Break after any dependent vowel but not before.
ा1
ि1
ी1
ु1
ू1
ृ1
ॄ1
ॢ1
ॣ1
े1
ै1
ो1
ौ1
% Break before or after any consonant.
1क
1ख
1ग
1घ
1ङ
1च
1छ
1ज
1झ
1ञ
1ट
1ठ
1ड
1ढ
1ण
1त
1थ
1द
1ध
1न
1प
1फ
1ब
1भ
1म
1य
1र
1ल
1ळ
1व
1श
1ष
1स
1ह
% Do not break before chandrabindu, anusvara, visarga, avagraha
% and accents.
2ँ
2ं
2
2ऽ
2॑
2॒
% Do not break either side of virama (may be within conjunct).
2्2
अति1
अधि1
अन1
अनु1
अन्1
अप1
अपि1
अभि1
अव1
1इय
उद्1
उप1
1का
चिर्1
1त्र
1त्व
दुर्1
दुस्1
नि1
निर्1
निस्1
पर1
परि1
प्र1
प्रति1
1ली
1वत्
वि1
सम्1
सु1

116
dictionaries/update.sh Executable file → Normal file
View File

@ -1,109 +1,7 @@
#!/bin/sh git clone https://git.libreoffice.org/dictionaries libreoffice-dictionaries
# Verify or export the pinned emacs-kp dictionary bundle. cd libreoffice-dictionaries
git pull
set -eu cd ..
find libreoffice-dictionaries -name "hyph_*\.dic" | xargs -I '{}' cp '{}' .
UPSTREAM_URL=https://github.com/LibreOffice/dictionaries.git sed -i 's/\r$//' *.dic
UPSTREAM_COMMIT=8fb8e794237cff49ec212023f96bcdb7d3fbf56c rename -- -Latn _Latn *-Latn.dic
DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
MANIFEST="$DIR/MANIFEST.tsv"
MODE=${1:-check}
OUTPUT=${2:-}
fail()
{
printf 'dictionary-update: %s\n' "$*" >&2
exit 1
}
sha256()
{
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
case "$MODE" in
check)
test -z "$OUTPUT" ||
fail "check mode does not accept an output directory"
;;
export)
test -n "$OUTPUT" ||
fail "usage: update.sh export OUTPUT_DIRECTORY"
if test -e "$OUTPUT"; then
test -d "$OUTPUT" || fail "output exists and is not a directory"
test -z "$(find "$OUTPUT" -mindepth 1 -print -quit)" ||
fail "output directory must be empty"
else
mkdir -p "$OUTPUT"
fi
;;
*)
fail "usage: update.sh [check | export OUTPUT_DIRECTORY]"
;;
esac
command -v git >/dev/null 2>&1 || fail "git is required"
test -f "$MANIFEST" || fail "MANIFEST.tsv is missing"
work=$(mktemp -d "${TMPDIR:-/tmp}/ekp-dictionary-update.XXXXXX")
cleanup_work()
{
find "$work" -depth -delete
}
trap cleanup_work EXIT HUP INT TERM
git -C "$work" init -q
git -C "$work" remote add origin "$UPSTREAM_URL"
git -C "$work" fetch -q --depth=1 origin "$UPSTREAM_COMMIT"
git -C "$work" rev-parse --verify FETCH_HEAD >/dev/null
test "$(git -C "$work" rev-parse FETCH_HEAD)" = "$UPSTREAM_COMMIT" ||
fail "fetched commit differs from the pin"
for evidence in \
eo/license-en.txt \
id/LICENSE-dict \
mr_IN/COPYING \
nl_NL/license_en_EN.txt \
ru_RU/README_ru_RU.txt
do
git -C "$work" cat-file -e "FETCH_HEAD:$evidence" ||
fail "pinned license evidence is missing: $evidence"
done
if test "$MODE" = export; then
cp "$MANIFEST" "$DIR/LICENSES.md" "$OUTPUT/"
for readme in "$DIR"/README_*.txt; do
cp "$readme" "$OUTPUT/"
done
fi
tab=$(printf '\t')
count=0
while IFS="$tab" read -r file source expected_hash _license _alternatives; do
case "$file" in
""|\#*) continue ;;
esac
count=$((count + 1))
normalized="$work/$file"
if test "$source" = legacy; then
cp "$DIR/$file" "$normalized"
else
git -C "$work" show "FETCH_HEAD:$source" > "$work/raw"
LC_ALL=C tr -d '\r' < "$work/raw" > "$normalized"
fi
test "$(sha256 "$normalized")" = "$expected_hash" ||
fail "$file differs from the pinned manifest"
if test "$MODE" = check; then
cmp -s "$DIR/$file" "$normalized" ||
fail "$file differs from normalized upstream bytes"
else
cp "$normalized" "$OUTPUT/$file"
fi
done < "$MANIFEST"
test "$count" -eq 49 || fail "manifest must contain 49 dictionaries"
printf 'dictionary-update: %s passed for %d pinned entries\n' "$MODE" "$count"

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,14 @@
;;; ekp-hyphen.el --- Liang hyphenation algorithm -*- lexical-binding: t; -*- ;;; ekp-hyphen.el --- Liang hyphenation algorithm -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Kinney Zhang ;; Copyright (C) 2024
;; Author: emacs-kp contributors
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
;; Keywords: text, hyphenation, typesetting ;; Keywords: text, hyphenation, typesetting
;; Package-Requires: ((emacs "27.1"))
;; 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: ;;; Commentary:
;; Implementation of Frank Liang's hyphenation algorithm. ;; Implementation of Frank Liang's hyphenation algorithm.
;; See: Liang, F.M. "Word Hy-phen-a-tion by Com-put-er" (1983) ;; See: Liang, F.M. "Word Hy-phen-a-tion by Com-put-er" (1983)
;; ;;
;; Usage: ;; Usage:
;; (ekp-hyphen-load-languages "/path/to/dictionaries") ;; (ekp-hyphen-load-languages "/path/to/dictionaries")
@ -48,54 +33,33 @@ LEFT/RIGHT: minimum chars before first / after last break."
;;; Global State ;;; Global State
(define-error 'ekp-hyphen-error "Hyphenation error")
(define-error 'ekp-hyphen-dictionary-not-found
"Hyphenation dictionary not found"
'ekp-hyphen-error)
(define-error 'ekp-hyphen-unsupported-pattern
"Dictionary uses unsupported replacement patterns"
'ekp-hyphen-error)
(defvar ekp-hyphen--cache (make-hash-table :test 'equal) (defvar ekp-hyphen--cache (make-hash-table :test 'equal)
"Cache: dictionary path -> compiled ekp-hyphen.") "Cache: dictionary path -> compiled ekp-hyphen.")
(defvar ekp-hyphen--unsupported-cache (make-hash-table :test 'equal)
"Cache: dictionary path -> unsupported replacement-pattern count.")
(defvar ekp-hyphen--langs (make-hash-table :test 'equal) (defvar ekp-hyphen--langs (make-hash-table :test 'equal)
"Registry: language code -> dictionary file path.") "Registry: language code -> dictionary file path.")
(defvar ekp-hyphen--langs-short (make-hash-table :test 'equal) (defvar ekp-hyphen--langs-short (make-hash-table :test 'equal)
"Fallback: short code (e.g., \"en\") -> first matching dict path.") "Fallback: short code (e.g., \"en\") -> first matching dict path.")
(defconst ekp-hyphen--cache-miss (make-symbol "ekp-hyphen-cache-miss")
"Sentinel distinguishing an absent word from a cached nil result.")
;;; Dictionary Loading ;;; Dictionary Loading
(defun ekp-hyphen--normalize-lang (lang)
"Return the normalized registry key for LANG."
(downcase (replace-regexp-in-string "-" "_" lang)))
(defun ekp-hyphen-load-languages (dir) (defun ekp-hyphen-load-languages (dir)
"Scan DIR for .dic files, populate language registry." "Scan DIR for .dic files, populate language registry."
(dolist (file (directory-files dir t "\\.dic\\'")) (dolist (file (directory-files dir t "\\.dic\\'"))
(let* ((name (file-name-nondirectory file)) (let* ((name (file-name-nondirectory file))
(lang (replace-regexp-in-string "\\(^hyph_\\|\\.dic$\\)" "" name)) (lang (replace-regexp-in-string "\\(^hyph_\\|\\.dic$\\)" "" name))
(normalized (ekp-hyphen--normalize-lang lang))
(short (car (split-string lang "_")))) (short (car (split-string lang "_"))))
(puthash lang file ekp-hyphen--langs) (puthash lang file ekp-hyphen--langs)
(puthash normalized file ekp-hyphen--langs)
(unless (gethash short ekp-hyphen--langs-short) (unless (gethash short ekp-hyphen--langs-short)
(puthash short file ekp-hyphen--langs-short))))) (puthash short file ekp-hyphen--langs-short)))))
(defun ekp-hyphen--resolve-lang (lang) (defun ekp-hyphen--resolve-lang (lang)
"Resolve LANG to dictionary path, trying exact then short forms." "Resolve LANG to dictionary path, trying exact then short forms."
(or (gethash lang ekp-hyphen--langs) (or (gethash lang ekp-hyphen--langs)
(let* ((norm (ekp-hyphen--normalize-lang lang)) (let* ((norm (downcase (replace-regexp-in-string "-" "_" lang)))
(parts (split-string norm "_")) (parts (split-string norm "_"))
found) found)
(setq found (gethash norm ekp-hyphen--langs))
(while (and parts (not found)) (while (and parts (not found))
(setq found (gethash (string-join parts "_") (setq found (gethash (string-join parts "_")
ekp-hyphen--langs-short) ekp-hyphen--langs-short)
@ -132,16 +96,10 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
(when (> end start) (when (> end start)
(list letters start (cl-subseq values start end)))))) (list letters start (cl-subseq values start end))))))
(defun ekp-hyphen--compile (path &optional language) (defun ekp-hyphen--compile (path)
"Compile dictionary at PATH into ekp-hyphen struct. "Compile dictionary at PATH into ekp-hyphen struct."
Honors the dictionary's LEFTHYPHENMIN / RIGHTHYPHENMIN declarations
\(minimum characters kept before/after any break e.g. en_US
declares 2/3, so \"quick-ly\" is not a valid break); absent
declarations default to 2/2.
Signal `ekp-hyphen-unsupported-pattern' for replacement patterns;
LANGUAGE identifies the dictionary in that condition."
(let ((patterns (make-hash-table :test 'equal)) (let ((patterns (make-hash-table :test 'equal))
(maxlen 0) (left 2) (right 2) (alternative-count 0)) (maxlen 0))
(with-temp-buffer (with-temp-buffer
(insert-file-contents path) (insert-file-contents path)
(forward-line 1) ; skip encoding line (forward-line 1) ; skip encoding line
@ -150,36 +108,22 @@ LANGUAGE identifies the dictionary in that condition."
(point) (line-end-position)))) (point) (line-end-position))))
(skip (or (string-empty-p line) (skip (or (string-empty-p line)
(string-match-p "^[%#]\\|HYPHENMIN" line) (string-match-p "^[%#]\\|HYPHENMIN" line)
(string-match-p "/" line)))) (string-match-p "/" line)))) ; skip alt patterns
(cond (unless skip
((string-match "^\\(LEFT\\|RIGHT\\)HYPHENMIN[ \t]*\\([0-9]+\\)"
line)
(let ((n (string-to-number (match-string 2 line))))
(if (equal (match-string 1 line) "LEFT")
(setq left n)
(setq right n))))
((and (not (string-match-p "^[%#]" line))
(string-match-p "/" line))
(cl-incf alternative-count))
(skip nil)
(t
;; Handle ^^XX hex escapes ;; Handle ^^XX hex escapes
(setq line (replace-regexp-in-string (setq line (replace-regexp-in-string
"\\^\\^\\([0-9a-fA-F]\\{2\\}\\)" "\\^\\^\\([0-9a-fA-F]\\{2\\}\\)"
(lambda (m) (string (string-to-number (lambda (m) (string (string-to-number
(match-string 1 m) 16))) (match-string 1 m) 16)))
line)) line))
(when-let* ((parsed (ekp-hyphen--parse-pattern line))) (when-let ((parsed (ekp-hyphen--parse-pattern line)))
(puthash (car parsed) (cdr parsed) patterns) (puthash (car parsed) (cdr parsed) patterns)
(setq maxlen (max maxlen (length (car parsed)))))))) (setq maxlen (max maxlen (length (car parsed)))))))
(forward-line 1))) (forward-line 1)))
(when (> alternative-count 0)
(signal 'ekp-hyphen-unsupported-pattern
(list (or language path) path alternative-count)))
(ekp-hyphen--create :patterns patterns (ekp-hyphen--create :patterns patterns
:cache (make-hash-table :test 'equal) :cache (make-hash-table :test 'equal)
:maxlen maxlen :maxlen maxlen
:left left :right right))) :left 2 :right 2)))
;;; Hyphenation Algorithm ;;; Hyphenation Algorithm
@ -209,65 +153,48 @@ LANGUAGE identifies the dictionary in that condition."
(nreverse result)))) (nreverse result))))
(defun ekp-hyphen--positions (h word) (defun ekp-hyphen--positions (h word)
"Return cached break positions for WORD using hyphenator H." "Get cached break positions for WORD."
(let* ((key (downcase word)) (let* ((key (downcase word))
(cache (ekp-hyphen-cache h)) (cache (ekp-hyphen-cache h)))
(cached (gethash key cache ekp-hyphen--cache-miss))) (or (gethash key cache)
(if (eq cached ekp-hyphen--cache-miss) (puthash key (ekp-hyphen--compute h word) cache))))
(puthash key (ekp-hyphen--compute h word) cache)
cached)))
;;; Public API ;;; Public API
(defun ekp-hyphen-create (&optional lang file left right) (defun ekp-hyphen-create (&optional lang file left right)
"Create hyphenator for LANG or dictionary FILE. "Create hyphenator for LANG or dictionary FILE.
LEFT/RIGHT override the minimum characters kept before/after breaks; LEFT/RIGHT: min chars before/after breaks (default 2)."
by default the dictionary's own LEFTHYPHENMIN/RIGHTHYPHENMIN apply
\(2/2 when it declares none)."
(let ((path (or (and lang (ekp-hyphen--resolve-lang lang)) file))) (let ((path (or (and lang (ekp-hyphen--resolve-lang lang)) file)))
(unless path (unless path (error "No dictionary for: %s" lang))
(signal 'ekp-hyphen-dictionary-not-found (list lang)))
(when-let* ((count (gethash path ekp-hyphen--unsupported-cache)))
(signal 'ekp-hyphen-unsupported-pattern
(list (or lang path) path count)))
(let ((h (or (gethash path ekp-hyphen--cache) (let ((h (or (gethash path ekp-hyphen--cache)
(condition-case err (puthash path (ekp-hyphen--compile path)
(puthash path (ekp-hyphen--compile path lang) ekp-hyphen--cache))))
ekp-hyphen--cache)
(ekp-hyphen-unsupported-pattern
(puthash path (nth 3 err)
ekp-hyphen--unsupported-cache)
(signal (car err) (cdr err)))))))
(if (or left right) (if (or left right)
(ekp-hyphen--create :patterns (ekp-hyphen-patterns h) (ekp-hyphen--create :patterns (ekp-hyphen-patterns h)
:cache (ekp-hyphen-cache h) :cache (ekp-hyphen-cache h)
:maxlen (ekp-hyphen-maxlen h) :maxlen (ekp-hyphen-maxlen h)
:left (or left (ekp-hyphen-left h)) :left (or left 2) :right (or right 2))
:right (or right (ekp-hyphen-right h)))
h)))) h))))
(defun ekp-hyphen-positions (h word) (defun ekp-hyphen-positions (h word)
"Return valid break positions in WORD for H, respecting margins." "Return valid break positions in WORD, respecting margins."
(let ((left (ekp-hyphen-left h)) (let ((left (ekp-hyphen-left h))
(right (- (length word) (ekp-hyphen-right h)))) (right (- (length word) (ekp-hyphen-right h))))
(cl-remove-if-not (lambda (p) (and (>= p left) (<= p right))) (cl-remove-if-not (lambda (p) (and (>= p left) (<= p right)))
(ekp-hyphen--positions h word)))) (ekp-hyphen--positions h word))))
(defun ekp-hyphen-inserted (h word &optional hyphen) (defun ekp-hyphen-inserted (h word &optional hyphen)
"Return WORD with HYPHEN inserted at H's break points." "Return WORD with HYPHEN inserted at break points."
(let ((positions (ekp-hyphen-positions h word))) (let ((hyphen (or hyphen "-")) (result word) (off 0))
(if (null positions) (dolist (pos (ekp-hyphen-positions h word))
word (setq result (concat (substring result 0 (+ pos off))
(let ((hyphen (or hyphen "-")) (start 0) parts) hyphen
(dolist (pos positions) (substring result (+ pos off)))
(push (substring word start pos) parts) off (+ off (length hyphen))))
(push hyphen parts) result))
(setq start pos))
(push (substring word start) parts)
(apply #'concat (nreverse parts))))))
(defun ekp-hyphen-boxes (h word) (defun ekp-hyphen-boxes (h word)
"Split WORD into syllables at H's break points." "Split WORD into syllables at break points."
(split-string (ekp-hyphen-inserted h word " ") " ")) (split-string (ekp-hyphen-inserted h word " ") " "))
(provide 'ekp-hyphen) (provide 'ekp-hyphen)

View File

@ -1,23 +1,7 @@
;;; ekp-utils.el --- Utility functions for EKP -*- lexical-binding: t; -*- ;;; ekp-utils.el --- Utility functions for EKP -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Kinney Zhang ;; Copyright (C) 2024
;; Author: emacs-kp contributors
;; 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: ;;; Commentary:
@ -57,10 +41,6 @@ information is available (batch mode, tty frames)."
(let ((family (face-attribute 'default :family))) (let ((family (face-attribute 'default :family)))
(if (stringp family) family (format "%s" family))))) (if (stringp family) family (format "%s" family)))))
;; GUI-only C function; absent in non-window-system builds (emacs-nox).
;; Call sites are guarded by `display-multi-font-p'.
(declare-function font-info "font.c" (name &optional frame))
(defun ekp-font-monospace-p (font-family) (defun ekp-font-monospace-p (font-family)
"Return non-nil if FONT-FAMILY appears to be monospace. "Return non-nil if FONT-FAMILY appears to be monospace.
Returns nil (unknown) when font information is unavailable." Returns nil (unknown) when font information is unavailable."
@ -76,7 +56,6 @@ Returns nil (unknown) when font information is unavailable."
(= (aref info 7) (aref info 11)))))))) (= (aref info 7) (aref info 11))))))))
(defun ekp-get-latin-letter (string) (defun ekp-get-latin-letter (string)
"Return the first Latin letter (a-z or A-Z) in STRING, or nil if none."
(with-temp-buffer (with-temp-buffer
(insert string) (insert string)
(goto-char (point-min)) (goto-char (point-min))
@ -89,7 +68,6 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point)))))) (buffer-substring (point) (1+ (point))))))
(defun ekp-get-cjk-letter (string) (defun ekp-get-cjk-letter (string)
"Return the first wide CJK character in STRING, or nil if none."
(with-temp-buffer (with-temp-buffer
(insert string) (insert string)
(goto-char (point-min)) (goto-char (point-min))
@ -103,9 +81,7 @@ Returns nil (unknown) when font information is unavailable."
(buffer-substring (point) (1+ (point)))))) (buffer-substring (point) (1+ (point))))))
(defun ekp-monospace-p (string) (defun ekp-monospace-p (string)
"Return the font family of STRING's Latin letters when monospace. "判断字符串中的拉丁字母的字体是否等宽,返回字体名称"
Return nil when that font is not monospace, or the default face
family when STRING contains no Latin letter."
(if-let* ((letter (ekp-get-latin-letter string)) (if-let* ((letter (ekp-get-latin-letter string))
(font-family (ekp-font-family letter))) (font-family (ekp-font-family letter)))
;; return monospace font family ;; return monospace font family
@ -114,34 +90,25 @@ family when STRING contains no Latin letter."
;; no latin letter in string, use default ;; no latin letter in string, use default
(face-attribute 'default :family))) (face-attribute 'default :family)))
(declare-function ekp--measured-width "ekp")
(defun ekp-word-spacing-pixel (string) (defun ekp-word-spacing-pixel (string)
"Return the pixel width of an inter-word space for STRING.
Use the blank glyph of STRING's Latin font; for a monospace font
that width is the space's own advance."
;; font is monospace, use the pixel of blank ;; font is monospace, use the pixel of blank
;; as word spacing pixel ;; as word spacing pixel
(if-let* ((font-family (ekp-monospace-p string))) (if-let ((font-family (ekp-monospace-p string)))
(ekp--measured-width (string-pixel-width
(propertize " " 'face `(:family ,font-family))) (propertize " " 'face `(:family ,font-family)))
(let* ((letter (ekp-get-latin-letter string)) (let* ((letter (ekp-get-latin-letter string))
(font-family (ekp-font-family letter))) (font-family (ekp-font-family letter)))
(ekp--measured-width (string-pixel-width
(propertize (propertize
" " 'face `(:family ,font-family)))))) " " 'face `(:family ,font-family))))))
(defun ekp-latin-font (string) (defun ekp-latin-font (string)
"Return the font family used for STRING's Latin letters. (if-let ((letter (ekp-get-latin-letter string)))
Fall back to the default face family when STRING has no Latin letter."
(if-let* ((letter (ekp-get-latin-letter string)))
(ekp-font-family letter) (ekp-font-family letter)
(face-attribute 'default :family))) (face-attribute 'default :family)))
(defun ekp-cjk-font (string) (defun ekp-cjk-font (string)
"Return the font family used for STRING's CJK characters. (if-let ((letter (ekp-get-cjk-letter string)))
Fall back to the family of a sample CJK glyph when STRING has none."
(if-let* ((letter (ekp-get-cjk-letter string)))
(ekp-font-family letter) (ekp-font-family letter)
(ekp-font-family ""))) (ekp-font-family "")))
@ -152,7 +119,7 @@ Fall back to the family of a sample CJK glyph when STRING has none."
(propertize " " 'display `(space :width (,pixel))))) (propertize " " 'display `(space :width (,pixel)))))
(defun ekp-cjk-fw-punct-p (str) (defun ekp-cjk-fw-punct-p (str)
"Return non-nil if STR begins with a CJK full-width punctuation char. "Return non-nil if STR starts with a CJK full-width punctuation char.
Full-width alphanumerics (, ) are NOT punctuation." Full-width alphanumerics (, ) are NOT punctuation."
(let ((char (seq-first str))) (let ((char (seq-first str)))
(and (and
@ -173,16 +140,16 @@ When STR is held as cjk-char, this checks if it still needs attachment."
(memq (get-char-code-property char 'general-category) (memq (get-char-code-property char 'general-category)
'(Ps Pi)))) '(Ps Pi))))
(defun ekp--flush-latin-word (parts boxes) (defun ekp--flush-latin-word (word boxes)
"Join reversed Latin PARTS once and push the word to BOXES." "Push latin WORD to BOXES if non-nil. Return updated boxes."
(if parts (cons (apply #'concat (nreverse parts)) boxes) boxes)) (if word (cons word boxes) boxes))
(defun ekp--flush-cjk-char (parts boxes) (defun ekp--flush-cjk-char (char boxes)
"Join reversed CJK PARTS once and push the character to BOXES." "Push CJK CHAR to BOXES if non-nil. Return updated boxes."
(if parts (cons (apply #'concat (nreverse parts)) boxes) boxes)) (if char (cons char boxes) boxes))
(defun ekp--flush-spaces (parts boxes prev-state next-width) (defun ekp--flush-spaces (spaces boxes prev-state next-width)
"Join reversed space PARTS and push them to BOXES based on context. "Push SPACES to BOXES based on context.
PREV-STATE: 1=latin, 2=CJK (previous content type). PREV-STATE: 1=latin, 2=CJK (previous content type).
NEXT-WIDTH: width of next character (1=latin, 2=CJK). NEXT-WIDTH: width of next character (1=latin, 2=CJK).
Rules: Rules:
@ -190,9 +157,8 @@ Rules:
- CJK involved (prev or next is CJK): preserve all spaces - CJK involved (prev or next is CJK): preserve all spaces
- Latin-Latin with single space: let glue handle it - Latin-Latin with single space: let glue handle it
- Latin-Latin with multiple spaces: preserve all but last" - Latin-Latin with multiple spaces: preserve all but last"
(when parts (when (and spaces (not (string-empty-p spaces)))
(let ((spaces (apply #'concat (nreverse parts))) (let ((cjk-involved (or (= prev-state 2) (= next-width 2))))
(cjk-involved (or (= prev-state 2) (= next-width 2))))
(cond (cond
;; Leading spaces (no previous boxes): preserve all ;; Leading spaces (no previous boxes): preserve all
((null boxes) ((null boxes)
@ -200,13 +166,6 @@ Rules:
;; CJK involved: preserve all spaces ;; CJK involved: preserve all spaces
(cjk-involved (cjk-involved
(setq boxes (cons spaces boxes))) (setq boxes (cons spaces boxes)))
;; Inside a no-break span: spacing is literal, glue would
;; stretch — preserve the run as a rigid space box.
((or (text-property-not-all
0 (length spaces) 'ekp-no-break nil spaces)
(text-property-not-all
0 (length spaces) 'ekp--literal-spacing nil spaces))
(setq boxes (cons spaces boxes)))
;; Latin-Latin with multiple spaces: preserve all but last ;; Latin-Latin with multiple spaces: preserve all but last
((> (length spaces) 1) ((> (length spaces) 1)
(setq boxes (cons (substring spaces 0 -1) boxes))) (setq boxes (cons (substring spaces 0 -1) boxes)))
@ -214,52 +173,69 @@ Rules:
(t nil)))) (t nil))))
boxes) boxes)
(defun ekp--flush-trailing-spaces (parts boxes) (defun ekp--flush-trailing-spaces (spaces boxes)
"Join reversed trailing space PARTS once and push them to BOXES." "Push all trailing SPACES to BOXES (for end of string)."
(if parts (if (and spaces (not (string-empty-p spaces)))
(cons (apply #'concat (nreverse parts)) boxes) (cons spaces boxes)
boxes)) boxes))
(defun ekp--zero-width-attaching-p (char) (defun ekp--zero-width-attaching-p (char)
"Return non-nil if zero-width CHAR must attach to the preceding text. "Return non-nil if zero-width CHAR must attach to the preceding text.
Combining marks (Mn/Mc/Me), ZWJ/ZWNJ, CGJ, variation selectors and Combining marks (Mn/Mc/Me), ZWJ/ZWNJ, CGJ and variation selectors
the word joiner attach to the previous character; other zero-width attach to the previous character; other zero-width characters (such
characters (such as zero-width space U+200B) are treated as as zero-width space U+200B) are treated as invisible break points."
invisible break points."
(or (memq (get-char-code-property char 'general-category) '(Mn Mc Me)) (or (memq (get-char-code-property char 'general-category) '(Mn Mc Me))
(memq char '(#x200C #x200D #x034F #x2060 #xFEFF)) (memq char '(#x200C #x200D #x034F))
(and (>= char #xFE00) (<= char #xFE0F)))) (and (>= char #xFE00) (<= char #xFE0F))))
(defun ekp--handle-latin-char (str state latin-word cjk-char boxes) (defun ekp--handle-latin-char (str state latin-word cjk-char boxes)
"Handle a latin (width=1) character STR. "Handle a latin (width=1) character.
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the Return (new-state new-latin-word new-cjk-char new-boxes)."
accumulators. Return (new-state new-latin-word new-cjk-char new-boxes)."
(if (= state 1) (if (= state 1)
;; Already in latin mode: accumulate ;; Already in latin mode: accumulate
(list 1 (cons str latin-word) nil boxes) (list 1 (concat latin-word str) nil boxes)
;; Was in CJK mode: flush held CJK char, switch to latin ;; Was in CJK mode: flush CJK char, switch to latin
(list 1 (list str) nil (ekp--flush-cjk-char cjk-char boxes)))) ;; If held cjk-char is opening punct, prepend it to the latin word
(if (and cjk-char (ekp-cjk-opening-punct-p cjk-char))
(list 1 (concat cjk-char str) nil boxes)
(list 1 str nil (ekp--flush-cjk-char cjk-char boxes)))))
(defun ekp--handle-cjk-char (str state latin-word cjk-char boxes) (defun ekp--handle-cjk-char (str state latin-word cjk-char boxes)
"Handle a CJK (width=2) character STR. "Handle a CJK (width=2) character.
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the Return (new-state new-latin-word new-cjk-char new-boxes)."
accumulators. Return (new-state new-latin-word new-cjk-char new-boxes).
Every CJK character punctuation included becomes its own box.
Kinsoku is enforced by the DP through per-gap break permissions
\(`ekp-para-breaks-allowed'), not by merging boxes."
(if (= state 1) (if (= state 1)
;; Was in latin mode: flush latin word, hold current CJK char ;; Was in latin mode: flush latin word, push CJK directly
(list 2 nil (list str) (ekp--flush-latin-word latin-word boxes)) (let ((new-boxes (ekp--flush-latin-word latin-word boxes)))
;; Already in CJK mode: flush held char, hold current (if (ekp-cjk-opening-punct-p str)
(list 2 nil (list str) (ekp--flush-cjk-char cjk-char boxes)))) ;; Opening punct: hold as cjk-char (will attach to next char)
(list 2 nil str new-boxes)
(list 2 nil nil (cons str new-boxes))))
;; Already in CJK mode
(cond
((ekp-cjk-opening-punct-p str)
;; Opening punct: cannot end a line (kinsoku rule).
;; If previous held char is also opening punct, concatenate them.
;; Otherwise flush previous and hold this opening punct.
(if (and cjk-char (ekp-cjk-opening-punct-p cjk-char))
(list 2 nil (concat cjk-char str) boxes)
(list 2 nil str (ekp--flush-cjk-char cjk-char boxes))))
((ekp-cjk-fw-punct-p str)
;; Closing/other punct: attaches to previous CJK char
(list 2 nil nil (cons (concat cjk-char str) boxes)))
(t
;; Regular CJK char: prepend any held opening punct
(if (and cjk-char (ekp-cjk-opening-punct-p cjk-char))
;; Previous was opening punct: combine with current char and hold.
;; Now last char is regular, so this won't be detected as opening.
(list 2 nil (concat cjk-char str) boxes)
;; Normal case: flush previous, hold current
(list 2 nil str (ekp--flush-cjk-char cjk-char boxes)))))))
(defun ekp-split-to-boxes (string) (defun ekp-split-to-boxes (string)
"Split STRING into typographic boxes. "Split STRING into typographic boxes.
Latin words become single boxes; CJK chars — punctuation included — Latin words become single boxes; CJK chars are individual boxes.
are individual boxes. Whitespace runs are preserved as separate Whitespace runs are preserved as separate boxes; CJK punctuation
boxes. Kinsoku is enforced later via per-gap break permissions, not attaches to its neighboring char per kinsoku rules."
by merging boxes."
(if (string-blank-p string) (if (string-blank-p string)
(vector string) (vector string)
(with-temp-buffer (with-temp-buffer
@ -267,9 +243,9 @@ by merging boxes."
(goto-char (point-min)) (goto-char (point-min))
(let ((state (char-width (seq-first string))) ; 1=latin, 2=CJK (let ((state (char-width (seq-first string))) ; 1=latin, 2=CJK
(prev-state 1) ; track previous content state for space handling (prev-state 1) ; track previous content state for space handling
latin-word ; reversed fragments for latin characters latin-word ; accumulator for latin characters
cjk-char ; reversed fragments for one CJK character cjk-char ; holds previous CJK char (for punct attachment)
spaces ; reversed fragments for a whitespace run spaces ; accumulator for whitespace runs
boxes) ; result list (built in reverse) boxes) ; result list (built in reverse)
(while (not (eobp)) (while (not (eobp))
(let* ((str (buffer-substring (point) (1+ (point)))) (let* ((str (buffer-substring (point) (1+ (point))))
@ -280,24 +256,24 @@ by merging boxes."
((and (= 0 width) (not (string-blank-p str)) ((and (= 0 width) (not (string-blank-p str))
(ekp--zero-width-attaching-p char)) (ekp--zero-width-attaching-p char))
(cond (cond
(latin-word (push str latin-word)) (latin-word (setq latin-word (concat latin-word str)))
(cjk-char (push str cjk-char)) (cjk-char (setq cjk-char (concat cjk-char str)))
(spaces (push str spaces)) (spaces (setq spaces (concat spaces str)))
(boxes (boxes (setcar boxes (concat (car boxes) str)))
(if (= prev-state 2)
(setq cjk-char (list str (pop boxes)) state 2)
(setq latin-word (list str (pop boxes)) state 1)))
;; String starts with a combining char: start an accumulator ;; String starts with a combining char: start an accumulator
(t (setq latin-word (list str) state 1)))) (t (setq latin-word str state 1))))
;; Whitespace or other zero-width: flush content, accumulate spaces ;; Whitespace or other zero-width: flush content, accumulate spaces
((or (string-blank-p str) (= 0 width)) ((or (string-blank-p str) (= 0 width))
(setq boxes (ekp--flush-cjk-char cjk-char boxes)) ;; Don't flush opening punct - keep it held for attachment to next char
(when cjk-char (setq prev-state 2)) (if (and cjk-char (ekp-cjk-opening-punct-p cjk-char))
(setq cjk-char nil) nil ; keep cjk-char as-is
(setq boxes (ekp--flush-cjk-char cjk-char boxes))
(when cjk-char (setq prev-state 2))
(setq cjk-char nil))
(setq boxes (ekp--flush-latin-word latin-word boxes)) (setq boxes (ekp--flush-latin-word latin-word boxes))
(when latin-word (setq prev-state 1)) (when latin-word (setq prev-state 1))
(setq latin-word nil) (setq latin-word nil)
(push str spaces)) (setq spaces (concat spaces str)))
;; Non-whitespace: flush spaces first, then handle char ;; Non-whitespace: flush spaces first, then handle char
(t (t
(setq boxes (ekp--flush-spaces spaces boxes prev-state width)) (setq boxes (ekp--flush-spaces spaces boxes prev-state width))
@ -324,6 +300,29 @@ by merging boxes."
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun ekp-start-process-with-callback
(process-name command-args callback
&optional output-buffer)
"Run COMMAND-ARGS asynchronously; call CALLBACK on success.
CALLBACK receives (PROCESS BUFFER). The output buffer is killed
after CALLBACK returns."
(let* ((buffer-name (generate-new-buffer-name
(or output-buffer "*EKP Process Output*")))
(process (apply #'start-process process-name
buffer-name command-args)))
(set-process-sentinel
process
(lambda (proc event)
(if (string-match-p "finished" event)
(when (memq (process-status proc) '(exit signal))
(unwind-protect
(funcall callback proc (process-buffer proc))
(when (buffer-live-p (process-buffer proc))
(kill-buffer (process-buffer proc)))))
(message "%s, please check %s" (string-trim event)
buffer-name))))
process))
(defun ekp--module-reload (module) (defun ekp--module-reload (module)
"Load MODULE from a temp copy to allow rebuilding." "Load MODULE from a temp copy to allow rebuilding."
(let ((tmpfile (make-temp-file (let ((tmpfile (make-temp-file
@ -338,13 +337,17 @@ by merging boxes."
(declare-function ekp-c-init "ext:ekp") (declare-function ekp-c-init "ext:ekp")
(declare-function ekp-c-version "ext:ekp") (declare-function ekp-c-version "ext:ekp")
(declare-function ekp-c-thread-count "ext:ekp") (declare-function ekp-c-thread-count "ext:ekp")
(declare-function ekp-c-load-hyphenator "ext:ekp")
(defvar ekp-c-module-loaded nil (defvar ekp-c-module-loaded nil
"Non-nil if C module is loaded.") "Non-nil if C module is loaded.")
(defvar ekp-c-hyphenator-index nil
"Index of the loaded hyphenator in C module.")
(defun ekp-c-module-dir () (defun ekp-c-module-dir ()
"Return the C module directory." "Return the C module directory."
(when-let* ((root-dir (ekp-root-dir))) (when-let ((root-dir (ekp-root-dir)))
(expand-file-name "ekp_c" root-dir))) (expand-file-name "ekp_c" root-dir)))
(defun ekp-c-module-file () (defun ekp-c-module-file ()
@ -358,28 +361,9 @@ by merging boxes."
(defalias 'ekp-c-module-reload #'ekp--module-reload (defalias 'ekp-c-module-reload #'ekp--module-reload
"Load MODULE from a temp copy to allow rebuilding.") "Load MODULE from a temp copy to allow rebuilding.")
(defconst ekp-c-module-required-version "1.6" (defconst ekp-c-module-required-version "1.1"
"Minimum C module version compatible with this Elisp code.") "Minimum C module version compatible with this Elisp code.")
(defun ekp--c-build-finished (profile process _event)
"Handle completion of PROCESS building C PROFILE."
(when (memq (process-status process) '(exit signal))
(if (and (eq (process-status process) 'exit)
(= (process-exit-status process) 0))
(condition-case error-data
(progn
(ekp-c-module-load)
(kill-buffer (process-buffer process))
(message "EKP C %s build succeeded" profile))
(error
(display-buffer (process-buffer process))
(message "EKP C build loaded unsuccessfully: %s"
(error-message-string error-data))))
(display-buffer (process-buffer process))
(message "EKP C %s build failed (status %d)"
profile (process-exit-status process)))))
;;;###autoload
(defun ekp-c-module-load () (defun ekp-c-module-load ()
"Load EKP C module if available. "Load EKP C module if available.
Refuses to enable a module older than Refuses to enable a module older than
@ -402,36 +386,36 @@ Run 'make' in ekp_c/ to rebuild; falling back to Elisp."
(ekp-c-version) (ekp-c-thread-count))))) (ekp-c-version) (ekp-c-thread-count)))))
(message "C module not found. Run 'make' in ekp_c/ directory.")))) (message "C module not found. Run 'make' in ekp_c/ directory."))))
;;;###autoload (defun ekp-c-load-dictionary (lang)
(defun ekp-c-module-build (&optional profile) "Load hyphenation dictionary for LANG into C module."
"Build the C module with make using PROFILE. (when ekp-c-module-loaded
PROFILE is one of `portable', `native', `debug', or `sanitize'; (let* ((root-dir (ekp-root-dir))
the default is `portable'." (dict-file (expand-file-name
(interactive (format "dictionaries/hyph_%s.dic" lang)
(list root-dir)))
(intern (when (file-exists-p dict-file)
(completing-read "C build profile: " (setq ekp-c-hyphenator-index
'("portable" "native" "debug" "sanitize") (ekp-c-load-hyphenator dict-file))
nil t nil nil "portable")))) (when ekp-c-hyphenator-index
(setq profile (or profile 'portable)) (message "Loaded hyphenator for %s (index %d)"
(unless (memq profile '(portable native debug sanitize)) lang ekp-c-hyphenator-index))))))
(user-error "Unknown EKP C build profile: %S" profile))
(let* ((module-dir (ekp-c-module-dir)) (defun ekp-c-module-build ()
(makefile (and module-dir "Build the C module using make."
(expand-file-name "Makefile" module-dir))) (interactive)
(make (executable-find "make"))) (let ((module-dir (ekp-c-module-dir)))
(unless (and makefile (file-exists-p makefile)) (if (and module-dir (file-exists-p
(user-error "Makefile not found in ekp_c/ directory")) (expand-file-name "Makefile" module-dir)))
(unless make (ekp-start-process-with-callback
(user-error "The make executable is not available")) "ekp-c-build"
(let* ((default-directory (file-name-as-directory module-dir)) (cond
(buffer (generate-new-buffer "*ekp-c-build*"))) ((eq system-type 'windows-nt)
(make-process `("cmd.exe" "/c" ,(format "cd %s && make" module-dir)))
:name (generate-new-buffer-name "ekp-c-build") (t `(,shell-file-name "-c" ,(format "cd %s && make" module-dir))))
:buffer buffer (lambda (_proc _buffer)
:command (list make (format "PROFILE=%s" profile)) (ekp-c-module-load)
:noquery t (message "ekp C module build success!")))
:sentinel (apply-partially #'ekp--c-build-finished profile))))) (error "Makefile not found in ekp_c/ directory"))))
(provide 'ekp-utils) (provide 'ekp-utils)

3212
ekp.el

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,5 @@
# Makefile for EKP C dynamic module # Makefile for EKP C dynamic module
# #
# Copyright (C) 2024-2026 Kinney Zhang
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Builds ekp.so (or ekp.dylib on macOS) for Emacs dynamic loading. # Builds ekp.so (or ekp.dylib on macOS) for Emacs dynamic loading.
# Uses pthread for multi-threading, optimizes for native CPU. # Uses pthread for multi-threading, optimizes for native CPU.
@ -17,39 +14,24 @@ endif
# Default to gcc on Windows if cc is not found # Default to gcc on Windows if cc is not found
ifeq ($(UNAME), Windows) ifeq ($(UNAME), Windows)
CC = gcc CC = gcc
# Point EMACS_ROOT at your Emacs installation (the directory that # Default Emacs root for this environment (can be overridden)
# contains include/emacs-module.h), e.g.: EMACS_ROOT ?= C:/Users/26289/Apps/emacs-30.2/emacs-30.2
# make EMACS_ROOT=C:/path/to/emacs-30.2 EMACS ?= $(EMACS_ROOT)/bin/emacs.exe
EMACS_ROOT ?=
ifeq ($(EMACS_ROOT),)
$(warning EMACS_ROOT is not set; emacs-module.h may not be found. \
Set it to your Emacs installation directory.)
endif
EMACS ?= emacs.exe
else else
CC ?= cc CC ?= cc
EMACS ?= emacs EMACS ?= emacs
endif endif
PROFILE ?= portable CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -O3 -fPIC
COMMON_CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -fPIC CFLAGS += -march=native -flto
COMMON_CFLAGS += -D_POSIX_C_SOURCE=200809L CFLAGS += -D_POSIX_C_SOURCE=200809L
ifeq ($(PROFILE),portable) # Debug build
PROFILE_CFLAGS := -O3 ifdef DEBUG
else ifeq ($(PROFILE),native) CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fPIC
PROFILE_CFLAGS := -O3 -march=native -flto CFLAGS += -fsanitize=address,undefined
else ifeq ($(PROFILE),debug)
PROFILE_CFLAGS := -O0 -g3
else ifeq ($(PROFILE),sanitize)
PROFILE_CFLAGS := -O1 -g3 -fno-omit-frame-pointer
PROFILE_CFLAGS += -fsanitize=address,undefined
else
$(error Unknown PROFILE '$(PROFILE)'; use portable, native, debug, or sanitize)
endif endif
CFLAGS := $(COMMON_CFLAGS) $(PROFILE_CFLAGS)
# Platform-specific settings # Platform-specific settings
ifeq ($(UNAME), Darwin) ifeq ($(UNAME), Darwin)
# macOS # macOS
@ -76,7 +58,7 @@ else
endif endif
# Source files # Source files
SRCS := ekp.c ekp_kp.c ekp_thread_pool.c SRCS := ekp.c ekp_kp.c ekp_hyphen.c ekp_paragraph.c ekp_thread_pool.c
OBJS := $(SRCS:.c=.o) OBJS := $(SRCS:.c=.o)
# Output # Output
@ -124,7 +106,6 @@ info:
@echo "MODULE: $(MODULE)" @echo "MODULE: $(MODULE)"
@echo "UNAME: $(UNAME)" @echo "UNAME: $(UNAME)"
@echo "EMACS: $(EMACS)" @echo "EMACS: $(EMACS)"
@echo "PROFILE: $(PROFILE)"
ifdef EMACS_INCLUDE ifdef EMACS_INCLUDE
@echo "EMACS_INCLUDE: $(EMACS_INCLUDE)" @echo "EMACS_INCLUDE: $(EMACS_INCLUDE)"
endif endif

View File

@ -1,18 +1,12 @@
# EKP C Dynamic Module # EKP C Dynamic Module
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.6). C implementation of the Knuth-Plass DP for emacs-kp (module version 1.1).
The division of labor: **Elisp owns all font-dependent data** The division of labor: **Elisp owns all font-dependent data**
(tokenization, pixel measurement, glue values, prefix sums); the C (tokenization, pixel measurement, glue values, prefix sums); the C
module runs only the O(n²) dynamic program. This keeps the two engines module runs only the O(n²) dynamic program. This keeps the two engines
byte-identical in output while making the hot loop native. byte-identical in output while making the hot loop native.
The module remains C deliberately. A Rust implementation would still expose
the Emacs C module ABI and consume the same Elisp-prepared vectors. Current
live-commit profiles put the candidate module call below one millisecond, so
a Rust rewrite would add Cargo, target, and packaging obligations without
removing the measured end-to-end owners.
## Architecture ## Architecture
``` ```
@ -21,102 +15,77 @@ ekp_c/
├── ekp.c # Emacs module entry point (emacs_module_init) ├── ekp.c # Emacs module entry point (emacs_module_init)
├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy ├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy
├── ekp_thread_pool.c # Thread pool (parallelism across paragraphs) ├── ekp_thread_pool.c # Thread pool (parallelism across paragraphs)
├── ekp_hyphen.c # Liang hyphenation (experimental path only)
├── ekp_paragraph.c # C-side tokenization (experimental path only)
└── Makefile └── Makefile
``` ```
Parallelism model: the DP for one paragraph is sequential (each Parallelism model: the DP for one paragraph is sequential (each
position depends on all earlier ones), so the thread pool parallelizes position depends on all earlier ones), so the thread pool parallelizes
across **paragraphs** via `ekp-c-break-batch` — the correct granularity, across **paragraphs** via `ekp-c-break-batch` — the correct granularity,
with zero synchronization in the inner loop. The pool is created with zero synchronization in the inner loop.
lazily on the first multi-paragraph batch, sized to the machine's
core count; a full queue blocks the submitter instead of dropping
tasks.
## Building ## Building
```bash ```bash
cd ekp_c cd ekp_c
make PROFILE=portable # default → ekp.dylib/.so/.dll make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)
``` ```
Requirements: C11 compiler, Emacs module headers, pthreads. Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.
Windows builds need MinGW-w64 (for pthreads) and
`make EMACS_ROOT=<path to your Emacs installation>`.
```bash ```bash
make PROFILE=native # local CPU + LTO; benchmark-only make DEBUG=1 # Debug build with sanitizers
make PROFILE=debug # -O0 with debug symbols
make PROFILE=sanitize # ASan/UBSan with frame pointers
make clean make clean
make info # includes the selected profile and final flags make info
``` ```
`portable` is the release and CI default and contains no
`-march=native`/LTO flags. An unknown profile is a make error. From Emacs,
`M-x ekp-c-module-build` prompts for the same profile names and starts make
with a direct argv plus `default-directory`; whitespace and shell
metacharacters in the checkout path are not interpreted.
## API (as used by ekp.el) ## API (as used by ekp.el)
```elisp ```elisp
(ekp-c-init) ; init global state (ekp-c-init) ; init global state + thread pool
(ekp-c-version) ; => "1.6" — checked by ekp-c-module-load (ekp-c-version) ; => "1.1" — checked by ekp-c-module-load
(ekp-c-thread-count) ; worker count (created lazily on first batch) (ekp-c-thread-count) ; => 8
(ekp-c-cleanup) (ekp-c-cleanup)
;; Synced automatically by ekp.el before every call: ;; Synced automatically by ekp.el before every call:
(ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO (ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO
&optional CONSEC-HYPHEN LAST-SHORT EXTRA-STRETCH &optional CONSEC-HYPHEN LAST-SHORT)
EMERGENCY-STRETCH)
;; Single paragraph (15 args): ;; Single paragraph (11 args):
(ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX (ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX
GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH
LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS LEAD-SPACES TRAIL-SPACES)
TAIL-PROTRUDES HYPHEN-PROTRUDE
FIRST-LINE-WIDTH)
;; => (BREAKS . TOTAL-COST) ;; => (BREAKS . TOTAL-COST)
;; Many paragraphs in parallel: vector of 15-element vectors ;; Many paragraphs in parallel: vector of 11-element vectors
(ekp-c-break-batch PARAGRAPHS) ; => vector of (BREAKS . COST) (ekp-c-break-batch PARAGRAPHS) ; => vector of (BREAKS . COST)
``` ```
`LEAD-SPACES` / `TRAIL-SPACES` are the space-box run widths that the `LEAD-SPACES` / `TRAIL-SPACES` are the space-box run widths that the
Elisp renderer strips from line edges; the DP excludes them from line Elisp renderer strips from line edges; the DP excludes them from line
metrics so both layers agree exactly (since 1.1). `FORBIDDEN-POS` metrics so both layers agree exactly (new in 1.1).
carries the kinsoku / no-break gap indices (since 1.2),
`TAIL-PROTRUDES` / `HYPHEN-PROTRUDE` the right-edge protrusion
allowances (since 1.4), and `FIRST-LINE-WIDTH` the width of line 0
for first-line indentation (since 1.5; pass the line width or ≤0
when no indent is active).
The DP uses the same two-pass strategy as the Elisp engine: a strict The DP uses the same two-pass strategy as the Elisp engine: a strict
Knuth-Plass pass, then — only when the paragraph end is unreachable — Knuth-Plass pass, then — only when the paragraph end is unreachable —
a final pass that adds finite background emergency stretch to ordinary a second pass permitting emergency single-box breaks, so overlong
underfull candidates and still scores them through the same unbreakable tokens can never make the result empty. Badness saturates
badness/fitness/demerits path. Separately, if an overfull candidate would at 10000 exactly like the Elisp side.
extinguish the final active path to a breakpoint and no non-overfull
candidate survives there, TeX-style artificial demerits install the best
provisional path with tight fitness and zero incremental cost. This is a
content-independent reachability safeguard, not a hard-atom scoring rule.
Thus an overlong unbreakable token cannot make the result empty, while
ordinary underfull choices remain global K-P decisions. Badness saturates at
10000 exactly like the Elisp side.
Failure behavior: the full schema is checked before extraction. Malformed ### Experimental: self-contained C path
direct API arguments signal `ekp-c-invalid-input`; allocation failure or an
unavailable DP result returns nil. `ekp.el` falls back only for nil. A module
signal propagates because it means the enabled backend contract is broken;
the dispatcher never hides it or silently produces a different layout.
Every public pixel/position integer must fit signed 32-bit range. The DP `ekp-c-break-lines` tokenizes and hyphenates in C
uses 64-bit intermediates for sums and differences, so valid extreme inputs (`ekp_paragraph.c`, `ekp_hyphen.c`) with a measurement callback into
cannot overflow when line width and protrusion are combined. Emacs. ekp.el does **not** use this path; its tokenizer is a
Validation uses one `extract_integer` call per integer value and clears the simplified approximation of `ekp-split-to-boxes`. Kept for
temporary non-local exit only when the value is not an integer; it does not experimentation.
round-trip through Lisp predicates and comparisons.
```elisp
(ekp-c-load-hyphenator "/path/to/hyph_en_US.dic") ; => index
(ekp-c-hyphenate 0 "hyphenation") ; => (2 5)
(ekp-c-break-lines "text..." 0 600 #'string-pixel-width)
```
## Performance ## Performance
@ -125,21 +94,13 @@ byte-compiled Elisp around the C calls, min of 3 cold-cache runs):
| Case | Elisp engine (compiled) | C engine | | Case | Elisp engine (compiled) | C engine |
|:----------------------------|------------------------:|---------:| |:----------------------------|------------------------:|---------:|
| justify text-zh.txt w=200 | 150 ms | 41 ms | | justify text-zh.txt w=200 | 96 ms | 57 ms |
| justify mixed text w=300 | 82 ms | 31 ms | | justify mixed text w=300 | 53 ms | 23 ms |
| range-justify zh 340380 | 529 ms | 106 ms | | range-justify zh 340380 | 294 ms | 75 ms |
| range-justify mix 280320 | 762 ms | 52 ms | | range-justify mix 280320 | 480 ms | 34 ms |
| DP only, text-zh w=400 | 30 ms | 2.5 ms | | DP only, text-zh w=400 | 15 ms | 1.3 ms |
The pure-DP speedup is ~12×; end-to-end gains are smaller because The pure-DP speedup is ~12× (1.3 ms vs 15 ms); end-to-end gains are
tokenization, measurement and rendering stay in Elisp. The C engine smaller because tokenization, measurement and rendering stay in Elisp.
matters most for `range-justify` (many widths per text) and The C engine matters most for `range-justify` (many widths per text)
multi-paragraph batches. Absolute numbers vary with the machine and and multi-paragraph batches.
power state; regenerate them with the two commands in DEVELOPER.md §9.
For task030's frozen 80-pixel structural-commit matrix, the C module layer
improved from 2.615/2.655 ms to 0.697/0.701 ms p95/p99 after one-pass integer
validation. With the production Elisp files byte-compiled, the complete
public append path measures 1.1581.326 ms p99 for C and 1.4291.438 ms for
pure Elisp on the same machine. These figures are separate from the
deliberately source-loaded, fully instrumented evaluator.

View File

@ -1,16 +1,6 @@
/* /*
* ekp.c - Emacs Knuth-Plass module entry point * ekp.c - Emacs Knuth-Plass module entry point
* *
* Copyright (C) 2024-2026 Kinney Zhang
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of emacs-kp, which 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.
* It is distributed WITHOUT ANY WARRANTY; see the GNU General Public
* License (COPYING) for details.
*
* This is the main entry file for the Emacs dynamic module. * This is the main entry file for the Emacs dynamic module.
* Naming follows Emacs module convention: module name = file name. * Naming follows Emacs module convention: module name = file name.
* (require 'ekp-c) loads ekp.so, entry point in ekp.c * (require 'ekp-c) loads ekp.so, entry point in ekp.c
@ -20,7 +10,6 @@
*/ */
#include "ekp_module.h" #include "ekp_module.h"
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@ -28,140 +17,25 @@
/* Required for Emacs modules */ /* Required for Emacs modules */
int plugin_is_GPL_compatible; int plugin_is_GPL_compatible;
/* Public array values are signed 32-bit pixels. The DP uses wider /* Cached Emacs environment for callbacks */
* intermediates; rejecting an out-of-range API value is safer than static emacs_env *current_env = NULL;
* silently changing it. */ static emacs_value measure_func = NULL;
static inline int32_t clamp32(intmax_t v)
/*
* Pixel measurement callback that calls back into Emacs
*/
static int32_t emacs_measure_string(const char *text, size_t len)
{ {
if (v > INT32_MAX) return INT32_MAX; if (!current_env || !measure_func)
if (v < INT32_MIN) return INT32_MIN; return len * 7; /* fallback: ~7 pixels per char */
return (int32_t)v;
}
static bool lisp_predicate(emacs_env *env, const char *name, emacs_value value) emacs_value str = current_env->make_string(current_env, text, len);
{ emacs_value result = current_env->funcall(current_env, measure_func, 1, &str);
emacs_value result = env->funcall(
env, env->intern(env, name), 1, (emacs_value[]){value});
return env->is_not_nil(env, result);
}
static bool i32_value_p(emacs_env *env, emacs_value value) if (current_env->non_local_exit_check(current_env) != emacs_funcall_exit_return)
{ return len * 7;
intmax_t integer = env->extract_integer(env, value);
if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
env->non_local_exit_clear(env);
return false;
}
return integer >= INT32_MIN && integer <= INT32_MAX;
}
static bool finite_number_p(emacs_env *env, emacs_value value) return (int32_t)current_env->extract_integer(current_env, result);
{
if (lisp_predicate(env, "floatp", value))
return isfinite(env->extract_float(env, value));
return i32_value_p(env, value);
}
static emacs_value signal_invalid_input(emacs_env *env, const char *message)
{
emacs_value text = env->make_string(env, message, strlen(message));
emacs_value data = env->funcall(
env, env->intern(env, "list"), 1, (emacs_value[]){text});
env->non_local_exit_signal(
env, env->intern(env, "ekp-c-invalid-input"), data);
return env->intern(env, "nil");
}
static bool i32_vector_p(emacs_env *env, emacs_value vector)
{
ptrdiff_t length = env->vec_size(env, vector);
for (ptrdiff_t i = 0; i < length; i++) {
if (!i32_value_p(env, env->vec_get(env, vector, i)))
return false;
}
return true;
}
static ptrdiff_t paragraph_field_length(int field, ptrdiff_t prefix_len)
{
switch (field) {
case 0: case 1: case 2: case 9: case 10: case 12:
return prefix_len;
case 3: case 4: case 5:
return prefix_len - 1;
default:
return -1;
}
}
static bool strictly_increasing_positions_p(emacs_env *env, emacs_value vector,
int32_t minimum, int32_t maximum)
{
ptrdiff_t length = env->vec_size(env, vector);
int32_t previous = minimum - 1;
for (ptrdiff_t i = 0; i < length; i++) {
int32_t position = clamp32(env->extract_integer(
env, env->vec_get(env, vector, i)));
if (position < minimum || position > maximum || position <= previous)
return false;
previous = position;
}
return true;
}
static const char *validate_paragraph_shapes(emacs_env *env, emacs_value *args)
{
static const int vectors[] = {0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12};
for (size_t i = 0; i < sizeof(vectors) / sizeof(vectors[0]); i++) {
if (!lisp_predicate(env, "vectorp", args[vectors[i]]))
return "EKP C paragraph array fields must be vectors";
}
ptrdiff_t prefix_len = env->vec_size(env, args[0]);
if (prefix_len <= 1 || prefix_len > INT32_MAX)
return "EKP C prefix vectors must contain 2..INT32_MAX elements";
for (int field = 0; field < 15; field++) {
ptrdiff_t expected = paragraph_field_length(field, prefix_len);
if (expected >= 0 && env->vec_size(env, args[field]) != expected)
return "EKP C paragraph vector lengths are inconsistent";
}
return NULL;
}
static const char *validate_paragraph_values(emacs_env *env, emacs_value *args)
{
static const int vectors[] = {0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12};
static const int scalars[] = {7, 8, 13, 14};
for (size_t i = 0; i < sizeof(vectors) / sizeof(vectors[0]); i++) {
if (!i32_vector_p(env, args[vectors[i]]))
return "EKP C paragraph vectors require signed 32-bit integers";
}
for (size_t i = 0; i < sizeof(scalars) / sizeof(scalars[0]); i++) {
if (!i32_value_p(env, args[scalars[i]]))
return "EKP C paragraph scalars require signed 32-bit integers";
}
if (env->extract_integer(env, args[8]) <= 0)
return "EKP C line width must be positive";
if (env->extract_integer(env, args[7]) < 0 ||
env->extract_integer(env, args[13]) < 0)
return "EKP C hyphen width and protrusion must be nonnegative";
int32_t box_count = clamp32(env->vec_size(env, args[0]) - 1);
if (!strictly_increasing_positions_p(env, args[6], 0, box_count - 1))
return "EKP C hyphen positions must be unique sorted box indices";
if (!strictly_increasing_positions_p(env, args[11], 1, box_count))
return "EKP C forbidden positions must be unique sorted gap indices";
return NULL;
}
static const char *validate_paragraph(emacs_env *env, emacs_value *args)
{
const char *error = validate_paragraph_shapes(env, args);
return error ? error : validate_paragraph_values(env, args);
} }
/* /*
@ -194,54 +68,74 @@ static emacs_value Fekp_c_cleanup(emacs_env *env, ptrdiff_t nargs,
} }
/* /*
* ekp-c-set-penalties: Set K-P parameters * ekp-c-load-hyphenator: Load hyphenation dictionary
*/ */
typedef struct { static emacs_value Fekp_c_load_hyphenator(emacs_env *env, ptrdiff_t nargs,
int32_t line; emacs_value *args, void *data)
int32_t hyphen;
int32_t fitness;
double last_ratio;
int32_t consecutive;
double last_short;
int32_t extra_stretch;
int32_t emergency_stretch;
} penalty_config_t;
static const char *parse_penalties(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, penalty_config_t *out)
{ {
if (!i32_value_p(env, args[0]) || !i32_value_p(env, args[1]) || (void)nargs; (void)data;
!i32_value_p(env, args[2]) || !finite_number_p(env, args[3]) ||
(nargs > 4 && !i32_value_p(env, args[4])) ||
(nargs > 5 && !finite_number_p(env, args[5])) ||
(nargs > 6 && !i32_value_p(env, args[6])) ||
(nargs > 7 && !i32_value_p(env, args[7])))
return "EKP C penalties require finite signed 32-bit numbers";
out->line = clamp32(env->extract_integer(env, args[0])); if (!ekp_global) {
out->hyphen = clamp32(env->extract_integer(env, args[1])); emacs_value signal = env->intern(env, "error");
out->fitness = clamp32(env->extract_integer(env, args[2])); emacs_value msg = env->make_string(env, "ekp-c not initialized", 21);
out->last_ratio = lisp_predicate(env, "floatp", args[3]) env->non_local_exit_signal(env, signal, msg);
? env->extract_float(env, args[3]) return env->intern(env, "nil");
: (double)env->extract_integer(env, args[3]); }
if (nargs > 4)
out->consecutive = clamp32(env->extract_integer(env, args[4]));
if (nargs > 5)
out->last_short = lisp_predicate(env, "floatp", args[5])
? env->extract_float(env, args[5])
: (double)env->extract_integer(env, args[5]);
out->extra_stretch = nargs > 6
? clamp32(env->extract_integer(env, args[6])) : 0;
out->emergency_stretch = nargs > 7
? clamp32(env->extract_integer(env, args[7])) : 0;
if (out->last_ratio < 0.0 || out->last_ratio > 1.0 || /* Get dictionary path */
out->last_short < 0.0 || out->extra_stretch < 0 || ptrdiff_t size = 0;
out->emergency_stretch < 0) env->copy_string_contents(env, args[0], NULL, &size);
return "EKP C ratios and stretch values are outside valid ranges"; char *path = malloc(size);
return NULL; if (!path)
return env->intern(env, "nil");
env->copy_string_contents(env, args[0], path, &size);
/* Load hyphenator */
ekp_hyphenator_t *h = ekp_hyphen_create(path);
free(path);
if (!h)
return env->intern(env, "nil");
/* Store in global state */
if (ekp_global->hyphenator_count < 32) {
ekp_global->hyphenators[ekp_global->hyphenator_count++] = h;
return env->make_integer(env, ekp_global->hyphenator_count - 1);
}
ekp_hyphen_destroy(h);
return env->intern(env, "nil");
} }
/*
* ekp-c-set-spacing: Set spacing parameters
*/
static emacs_value Fekp_c_set_spacing(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 9) {
return env->intern(env, "nil");
}
ekp_global->spacing.lws_ideal = env->extract_integer(env, args[0]);
ekp_global->spacing.lws_stretch = env->extract_integer(env, args[1]);
ekp_global->spacing.lws_shrink = env->extract_integer(env, args[2]);
ekp_global->spacing.mws_ideal = env->extract_integer(env, args[3]);
ekp_global->spacing.mws_stretch = env->extract_integer(env, args[4]);
ekp_global->spacing.mws_shrink = env->extract_integer(env, args[5]);
ekp_global->spacing.cws_ideal = env->extract_integer(env, args[6]);
ekp_global->spacing.cws_stretch = env->extract_integer(env, args[7]);
ekp_global->spacing.cws_shrink = env->extract_integer(env, args[8]);
return env->intern(env, "t");
}
/*
* ekp-c-set-penalties: Set K-P parameters
*/
static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs, static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data) emacs_value *args, void *data)
{ {
@ -250,26 +144,140 @@ static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
if (!ekp_global || nargs < 4) if (!ekp_global || nargs < 4)
return env->intern(env, "nil"); return env->intern(env, "nil");
penalty_config_t config = { ekp_global->line_penalty = env->extract_integer(env, args[0]);
.consecutive = ekp_global->consec_hyphen_penalty, ekp_global->hyphen_penalty = env->extract_integer(env, args[1]);
.last_short = ekp_global->last_line_short_penalty ekp_global->fitness_penalty = env->extract_integer(env, args[2]);
}; ekp_global->last_line_ratio = env->extract_float(env, args[3]);
const char *error = parse_penalties(env, nargs, args, &config); if (nargs > 4)
if (error) ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]);
return signal_invalid_input(env, error); if (nargs > 5)
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
ekp_global->line_penalty = config.line;
ekp_global->hyphen_penalty = config.hyphen;
ekp_global->fitness_penalty = config.fitness;
ekp_global->last_line_ratio = config.last_ratio;
ekp_global->consec_hyphen_penalty = config.consecutive;
ekp_global->last_line_short_penalty = config.last_short;
ekp_global->extra_stretch = config.extra_stretch;
ekp_global->emergency_stretch = config.emergency_stretch;
return env->intern(env, "t"); return env->intern(env, "t");
} }
/*
* ekp-c-hyphenate: Get hyphenation positions for a word
*/
static emacs_value Fekp_c_hyphenate(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 2)
return env->intern(env, "nil");
intmax_t h_idx = env->extract_integer(env, args[0]);
if (h_idx < 0 || (size_t)h_idx >= ekp_global->hyphenator_count)
return env->intern(env, "nil");
ekp_hyphenator_t *h = ekp_global->hyphenators[h_idx];
/* Get word */
ptrdiff_t size = 0;
env->copy_string_contents(env, args[1], NULL, &size);
char *word = malloc(size);
if (!word)
return env->intern(env, "nil");
env->copy_string_contents(env, args[1], word, &size);
/* Hyphenate */
int8_t positions[EKP_MAX_WORD_LEN];
int count = ekp_hyphen_word(h, word, size - 1, positions, EKP_MAX_WORD_LEN);
free(word);
/* Build result list */
emacs_value result = env->intern(env, "nil");
emacs_value cons_sym = env->intern(env, "cons");
for (int i = count - 1; i >= 0; i--) {
emacs_value pos = env->make_integer(env, positions[i]);
emacs_value args2[2] = {pos, result};
result = env->funcall(env, cons_sym, 2, args2);
}
return result;
}
/*
* ekp-c-break-lines: Core line breaking function
*
* Args: (string hyphenator-index line-width measure-func)
* Returns: (breaks . total-cost) where breaks is a list
*/
static emacs_value Fekp_c_break_lines(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)data;
if (!ekp_global || nargs < 4)
return env->intern(env, "nil");
/* Get string */
ptrdiff_t size = 0;
env->copy_string_contents(env, args[0], NULL, &size);
char *text = malloc(size);
if (!text)
return env->intern(env, "nil");
env->copy_string_contents(env, args[0], text, &size);
size_t text_len = size - 1;
/* Get hyphenator */
intmax_t h_idx = env->extract_integer(env, args[1]);
ekp_hyphenator_t *h = NULL;
if (h_idx >= 0 && (size_t)h_idx < ekp_global->hyphenator_count)
h = ekp_global->hyphenators[h_idx];
/* Get line width */
int32_t line_width = env->extract_integer(env, args[2]);
/* Get measure function */
current_env = env;
measure_func = args[3];
/* Create paragraph */
ekp_paragraph_t *para = ekp_para_create(text, text_len, h, emacs_measure_string);
free(text);
if (!para) {
current_env = NULL;
measure_func = NULL;
return env->intern(env, "nil");
}
/* Break lines */
ekp_result_t *result = ekp_break_lines(para, line_width);
current_env = NULL;
measure_func = NULL;
if (!result) {
ekp_para_destroy(para);
return env->intern(env, "nil");
}
/* Build result: ((breaks...) . cost) */
emacs_value breaks_list = env->intern(env, "nil");
emacs_value cons_sym = env->intern(env, "cons");
for (size_t i = result->break_count; i > 0; i--) {
emacs_value brk = env->make_integer(env, result->breaks[i - 1]);
emacs_value args2[2] = {brk, breaks_list};
breaks_list = env->funcall(env, cons_sym, 2, args2);
}
emacs_value cost = env->make_float(env, result->total_cost);
emacs_value args2[2] = {breaks_list, cost};
emacs_value final = env->funcall(env, cons_sym, 2, args2);
ekp_result_destroy(result);
ekp_para_destroy(para);
return final;
}
/* /*
* ekp-c-version: Return module version * ekp-c-version: Return module version
*/ */
@ -292,23 +300,17 @@ static emacs_value Fekp_c_thread_count(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data) emacs_value *args, void *data)
{ {
(void)nargs; (void)args; (void)data; (void)nargs; (void)args; (void)data;
/* Pool is created lazily; report its actual size once it exists, return env->make_integer(env, EKP_THREAD_POOL_SIZE);
* else the size it will get. */
if (ekp_global && ekp_global->pool)
return env->make_integer(env, (intmax_t)ekp_global->pool->thread_count);
return env->make_integer(env, (intmax_t)ekp_pool_default_threads());
} }
/* /*
* ekp-c-break-with-arrays: Pure DP with Elisp-provided prefix arrays * ekp-c-break-with-arrays: Pure DP with Elisp-provided prefix arrays
* *
* Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks * Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks glue-stretches
* glue-stretches hyphen-positions hyphen-width line-width * hyphen-positions hyphen-width line-width)
* lead-spaces trail-spaces forbidden-positions tail-protrudes
* hyphen-protrude first-line-width)
* *
* Array sizes must be consistent: * All 6 arrays must have consistent sizes:
* - ideal/min/max-prefix, lead/trail-spaces, tail-protrudes: n+1 * - ideal/min/max-prefix: (n+1) elements
* - glue-ideals/shrinks/stretches: n elements * - glue-ideals/shrinks/stretches: n elements
* *
* Returns: (breaks . total-cost) where breaks is a list of box indices. * Returns: (breaks . total-cost) where breaks is a list of box indices.
@ -321,15 +323,13 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
{ {
(void)data; (void)data;
if (!ekp_global || nargs < 15) if (!ekp_global || nargs < 11)
return env->intern(env, "nil"); return env->intern(env, "nil");
const char *validation_error = validate_paragraph(env, args);
if (validation_error)
return signal_invalid_input(env, validation_error);
/* Get prefix array sizes (n+1 elements) */ /* Get prefix array sizes (n+1 elements) */
ptrdiff_t prefix_len = env->vec_size(env, args[0]); ptrdiff_t prefix_len = env->vec_size(env, args[0]);
if (prefix_len <= 1)
return env->intern(env, "nil");
size_t n = prefix_len - 1; /* number of boxes */ size_t n = prefix_len - 1; /* number of boxes */
@ -354,18 +354,18 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
/* Extract prefix arrays */ /* Extract prefix arrays */
for (ptrdiff_t i = 0; i < prefix_len; i++) { for (ptrdiff_t i = 0; i < prefix_len; i++) {
ideal_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i))); ideal_prefix[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
min_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i))); min_prefix[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
max_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i))); max_prefix[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
lead_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i))); lead_spaces[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
trail_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i))); trail_spaces[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
} }
/* Extract glue arrays */ /* Extract glue arrays */
for (size_t i = 0; i < n; i++) { for (size_t i = 0; i < n; i++) {
glue_ideals[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i))); glue_ideals[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
glue_shrinks[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i))); glue_shrinks[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
glue_stretches[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i))); glue_stretches[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
} }
/* Get hyphen positions vector */ /* Get hyphen positions vector */
@ -375,51 +375,13 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
hyph_pos = malloc(hyph_count * sizeof(int32_t)); hyph_pos = malloc(hyph_count * sizeof(int32_t));
if (hyph_pos) { if (hyph_pos) {
for (ptrdiff_t i = 0; i < hyph_count; i++) { for (ptrdiff_t i = 0; i < hyph_count; i++) {
hyph_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i))); hyph_pos[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
} }
} }
} }
int32_t hyph_width = clamp32(env->extract_integer(env, args[7])); int32_t hyph_width = env->extract_integer(env, args[7]);
int32_t line_width = clamp32(env->extract_integer(env, args[8])); int32_t line_width = env->extract_integer(env, args[8]);
/* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */
int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t));
if (tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) {
tail_pro[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i)));
}
}
int32_t hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
int32_t first_line_width = clamp32(env->extract_integer(env, args[14]));
/* Forbidden break positions (sorted gap indices, may be empty) */
ptrdiff_t forb_count = env->vec_size(env, args[11]);
int32_t *forb_pos = NULL;
if (forb_count > 0) {
forb_pos = malloc(forb_count * sizeof(int32_t));
if (forb_pos) {
for (ptrdiff_t i = 0; i < forb_count; i++) {
forb_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i)));
}
}
}
/* One consolidated gate: any partial allocation above (silent
* "no kinsoku / no hyphenation" degradation) or a pending Lisp
* signal from a bad element type must fail the whole call the
* Elisp engine is the correct fallback, not a subtly different
* layout. */
if ((hyph_count > 0 && !hyph_pos) ||
(forb_count > 0 && !forb_pos) ||
!tail_pro ||
env->non_local_exit_check(env) != emacs_funcall_exit_return) {
free(ideal_prefix); free(min_prefix); free(max_prefix);
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
free(lead_spaces); free(trail_spaces);
free(hyph_pos); free(forb_pos); free(tail_pro);
return env->intern(env, "nil");
}
/* Call the pure DP function */ /* Call the pure DP function */
ekp_result_t *result = ekp_break_with_prefixes( ekp_result_t *result = ekp_break_with_prefixes(
@ -428,16 +390,12 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
n, n,
hyph_pos, hyph_count > 0 ? (size_t)hyph_count : 0, hyph_pos, hyph_count > 0 ? (size_t)hyph_count : 0,
hyph_width, line_width, hyph_width, line_width,
lead_spaces, trail_spaces, lead_spaces, trail_spaces);
forb_pos, (forb_pos && forb_count > 0) ? (size_t)forb_count : 0,
tail_pro, hyphen_protrude, first_line_width);
free(ideal_prefix); free(min_prefix); free(max_prefix); free(ideal_prefix); free(min_prefix); free(max_prefix);
free(glue_ideals); free(glue_shrinks); free(glue_stretches); free(glue_ideals); free(glue_shrinks); free(glue_stretches);
free(lead_spaces); free(trail_spaces); free(lead_spaces); free(trail_spaces);
free(hyph_pos); free(hyph_pos);
free(forb_pos);
free(tail_pro);
if (!result) if (!result)
return env->intern(env, "nil"); return env->intern(env, "nil");
@ -470,10 +428,7 @@ static bool extract_paragraph_data(
int32_t **glue_ideals, int32_t **glue_shrinks, int32_t **glue_stretches, int32_t **glue_ideals, int32_t **glue_shrinks, int32_t **glue_stretches,
int32_t **hyph_pos, size_t *n, ptrdiff_t *hyph_count, int32_t **hyph_pos, size_t *n, ptrdiff_t *hyph_count,
int32_t *hyph_width, int32_t *line_width, int32_t *hyph_width, int32_t *line_width,
int32_t **lead_spaces, int32_t **trail_spaces, int32_t **lead_spaces, int32_t **trail_spaces)
int32_t **forb_pos, ptrdiff_t *forb_count,
int32_t **tail_pro, int32_t *hyphen_protrude,
int32_t *first_line_width)
{ {
ptrdiff_t prefix_len = env->vec_size(env, args[0]); ptrdiff_t prefix_len = env->vec_size(env, args[0]);
if (prefix_len <= 1) if (prefix_len <= 1)
@ -500,17 +455,17 @@ static bool extract_paragraph_data(
} }
for (ptrdiff_t i = 0; i < prefix_len; i++) { for (ptrdiff_t i = 0; i < prefix_len; i++) {
(*ideal_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i))); (*ideal_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
(*min_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i))); (*min_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
(*max_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i))); (*max_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
(*lead_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i))); (*lead_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
(*trail_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i))); (*trail_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
} }
for (size_t i = 0; i < *n; i++) { for (size_t i = 0; i < *n; i++) {
(*glue_ideals)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i))); (*glue_ideals)[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
(*glue_shrinks)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i))); (*glue_shrinks)[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
(*glue_stretches)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i))); (*glue_stretches)[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
} }
*hyph_count = env->vec_size(env, args[6]); *hyph_count = env->vec_size(env, args[6]);
@ -519,48 +474,13 @@ static bool extract_paragraph_data(
*hyph_pos = malloc(*hyph_count * sizeof(int32_t)); *hyph_pos = malloc(*hyph_count * sizeof(int32_t));
if (*hyph_pos) { if (*hyph_pos) {
for (ptrdiff_t i = 0; i < *hyph_count; i++) { for (ptrdiff_t i = 0; i < *hyph_count; i++) {
(*hyph_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i))); (*hyph_pos)[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
} }
} }
} }
*hyph_width = clamp32(env->extract_integer(env, args[7])); *hyph_width = env->extract_integer(env, args[7]);
*line_width = clamp32(env->extract_integer(env, args[8])); *line_width = env->extract_integer(env, args[8]);
*forb_count = env->vec_size(env, args[11]);
*forb_pos = NULL;
if (*forb_count > 0) {
*forb_pos = malloc(*forb_count * sizeof(int32_t));
if (*forb_pos) {
for (ptrdiff_t i = 0; i < *forb_count; i++) {
(*forb_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i)));
}
}
}
*tail_pro = malloc(prefix_len * sizeof(int32_t));
if (*tail_pro) {
for (ptrdiff_t i = 0; i < prefix_len; i++) {
(*tail_pro)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i)));
}
}
*hyphen_protrude = clamp32(env->extract_integer(env, args[13]));
*first_line_width = clamp32(env->extract_integer(env, args[14]));
if ((*hyph_count > 0 && !*hyph_pos) ||
(*forb_count > 0 && !*forb_pos) ||
!*tail_pro ||
env->non_local_exit_check(env) != emacs_funcall_exit_return) {
free(*ideal_prefix); free(*min_prefix); free(*max_prefix);
free(*glue_ideals); free(*glue_shrinks); free(*glue_stretches);
free(*lead_spaces); free(*trail_spaces);
free(*hyph_pos); free(*forb_pos); free(*tail_pro);
*ideal_prefix = *min_prefix = *max_prefix = NULL;
*glue_ideals = *glue_shrinks = *glue_stretches = NULL;
*lead_spaces = *trail_spaces = NULL;
*hyph_pos = *forb_pos = *tail_pro = NULL;
return false;
}
return true; return true;
} }
@ -569,12 +489,9 @@ static bool extract_paragraph_data(
* ekp-c-break-batch: Process multiple paragraphs in parallel * ekp-c-break-batch: Process multiple paragraphs in parallel
* *
* Args: vector of (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks * Args: vector of (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks
* glue-stretches hyphen-positions hyphen-width line-width * glue-stretches hyphen-positions hyphen-width line-width)
* lead-spaces trail-spaces forbidden-positions tail-protrudes
* hyphen-protrude first-line-width)
* *
* Each element is a vector of 15 elements (same as * Each element is a vector of 9 elements (same as ekp-c-break-with-arrays args).
* ekp-c-break-with-arrays args).
* Returns vector of (breaks . total-cost) for each paragraph. * Returns vector of (breaks . total-cost) for each paragraph.
* *
* This is the high-performance API for processing multi-paragraph text. * This is the high-performance API for processing multi-paragraph text.
@ -587,31 +504,10 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
if (!ekp_global || nargs < 1) if (!ekp_global || nargs < 1)
return env->intern(env, "nil"); return env->intern(env, "nil");
if (!lisp_predicate(env, "vectorp", args[0]))
return signal_invalid_input(
env, "EKP C batch input must be a vector");
ptrdiff_t para_count = env->vec_size(env, args[0]); ptrdiff_t para_count = env->vec_size(env, args[0]);
if (para_count <= 0) if (para_count <= 0)
return env->intern(env, "nil"); return env->intern(env, "nil");
/* Validate the complete batch before allocating or extracting any
* paragraph, so malformed input cannot leave a partial batch. */
for (ptrdiff_t p = 0; p < para_count; p++) {
emacs_value para_vec = env->vec_get(env, args[0], p);
if (!lisp_predicate(env, "vectorp", para_vec) ||
env->vec_size(env, para_vec) != 15)
return signal_invalid_input(
env, "Each EKP C batch paragraph must contain 15 fields");
emacs_value para_args[15];
for (int i = 0; i < 15; i++)
para_args[i] = env->vec_get(env, para_vec, i);
const char *validation_error = validate_paragraph(env, para_args);
if (validation_error)
return signal_invalid_input(env, validation_error);
}
/* Allocate batch inputs and temporary storage */ /* Allocate batch inputs and temporary storage */
ekp_batch_input_t *inputs = calloc(para_count, sizeof(ekp_batch_input_t)); ekp_batch_input_t *inputs = calloc(para_count, sizeof(ekp_batch_input_t));
int32_t **all_ideal = calloc(para_count, sizeof(int32_t *)); int32_t **all_ideal = calloc(para_count, sizeof(int32_t *));
@ -623,15 +519,13 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
int32_t **all_hyph = calloc(para_count, sizeof(int32_t *)); int32_t **all_hyph = calloc(para_count, sizeof(int32_t *));
int32_t **all_lead = calloc(para_count, sizeof(int32_t *)); int32_t **all_lead = calloc(para_count, sizeof(int32_t *));
int32_t **all_trail = calloc(para_count, sizeof(int32_t *)); int32_t **all_trail = calloc(para_count, sizeof(int32_t *));
int32_t **all_forb = calloc(para_count, sizeof(int32_t *));
int32_t **all_pro = calloc(para_count, sizeof(int32_t *));
if (!inputs || !all_ideal || !all_min || !all_max || if (!inputs || !all_ideal || !all_min || !all_max ||
!all_glue_i || !all_glue_sh || !all_glue_st || !all_hyph || !all_glue_i || !all_glue_sh || !all_glue_st || !all_hyph ||
!all_lead || !all_trail || !all_forb || !all_pro) { !all_lead || !all_trail) {
free(inputs); free(all_ideal); free(all_min); free(all_max); free(inputs); free(all_ideal); free(all_min); free(all_max);
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph); free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
free(all_lead); free(all_trail); free(all_forb); free(all_pro); free(all_lead); free(all_trail);
return env->intern(env, "nil"); return env->intern(env, "nil");
} }
@ -639,35 +533,31 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
for (ptrdiff_t p = 0; p < para_count; p++) { for (ptrdiff_t p = 0; p < para_count; p++) {
emacs_value para_vec = env->vec_get(env, args[0], p); emacs_value para_vec = env->vec_get(env, args[0], p);
/* Extract 15 arguments from this paragraph's vector */ /* Extract 11 arguments from this paragraph's vector */
emacs_value para_args[15]; emacs_value para_args[11];
for (int i = 0; i < 15; i++) { for (int i = 0; i < 11; i++) {
para_args[i] = env->vec_get(env, para_vec, i); para_args[i] = env->vec_get(env, para_vec, i);
} }
size_t n; size_t n;
ptrdiff_t hyph_count, forb_count; ptrdiff_t hyph_count;
int32_t hyph_width, line_width, hyphen_protrude, first_line_width; int32_t hyph_width, line_width;
if (!extract_paragraph_data(env, para_args, if (!extract_paragraph_data(env, para_args,
&all_ideal[p], &all_min[p], &all_max[p], &all_ideal[p], &all_min[p], &all_max[p],
&all_glue_i[p], &all_glue_sh[p], &all_glue_st[p], &all_glue_i[p], &all_glue_sh[p], &all_glue_st[p],
&all_hyph[p], &n, &hyph_count, &all_hyph[p], &n, &hyph_count,
&hyph_width, &line_width, &hyph_width, &line_width,
&all_lead[p], &all_trail[p], &all_lead[p], &all_trail[p])) {
&all_forb[p], &forb_count,
&all_pro[p], &hyphen_protrude,
&first_line_width)) {
/* Cleanup on failure */ /* Cleanup on failure */
for (ptrdiff_t j = 0; j < p; j++) { for (ptrdiff_t j = 0; j < p; j++) {
free(all_ideal[j]); free(all_min[j]); free(all_max[j]); free(all_ideal[j]); free(all_min[j]); free(all_max[j]);
free(all_glue_i[j]); free(all_glue_sh[j]); free(all_glue_st[j]); free(all_glue_i[j]); free(all_glue_sh[j]); free(all_glue_st[j]);
free(all_hyph[j]); free(all_lead[j]); free(all_trail[j]); free(all_hyph[j]); free(all_lead[j]); free(all_trail[j]);
free(all_forb[j]); free(all_pro[j]);
} }
free(inputs); free(all_ideal); free(all_min); free(all_max); free(inputs); free(all_ideal); free(all_min); free(all_max);
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph); free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
free(all_lead); free(all_trail); free(all_forb); free(all_pro); free(all_lead); free(all_trail);
return env->intern(env, "nil"); return env->intern(env, "nil");
} }
@ -684,12 +574,6 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
inputs[p].line_width = line_width; inputs[p].line_width = line_width;
inputs[p].lead_spaces = all_lead[p]; inputs[p].lead_spaces = all_lead[p];
inputs[p].trail_spaces = all_trail[p]; inputs[p].trail_spaces = all_trail[p];
inputs[p].forbidden_positions = all_forb[p];
inputs[p].forbidden_count =
(all_forb[p] && forb_count > 0) ? (size_t)forb_count : 0;
inputs[p].tail_protrudes = all_pro[p];
inputs[p].hyphen_protrude = hyphen_protrude;
inputs[p].first_line_width = first_line_width;
} }
/* Process all paragraphs in parallel */ /* Process all paragraphs in parallel */
@ -700,11 +584,10 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
free(all_ideal[p]); free(all_min[p]); free(all_max[p]); free(all_ideal[p]); free(all_min[p]); free(all_max[p]);
free(all_glue_i[p]); free(all_glue_sh[p]); free(all_glue_st[p]); free(all_glue_i[p]); free(all_glue_sh[p]); free(all_glue_st[p]);
free(all_hyph[p]); free(all_lead[p]); free(all_trail[p]); free(all_hyph[p]); free(all_lead[p]); free(all_trail[p]);
free(all_forb[p]); free(all_pro[p]);
} }
free(inputs); free(all_ideal); free(all_min); free(all_max); free(inputs); free(all_ideal); free(all_min); free(all_max);
free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph); free(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
free(all_lead); free(all_trail); free(all_forb); free(all_pro); free(all_lead); free(all_trail);
if (!results) if (!results)
return env->intern(env, "nil"); return env->intern(env, "nil");
@ -772,12 +655,6 @@ int emacs_module_init(struct emacs_runtime *runtime)
if ((size_t)env->size < sizeof(*env)) if ((size_t)env->size < sizeof(*env))
return 2; return 2;
emacs_value error_name = env->intern(env, "ekp-c-invalid-input");
emacs_value error_message = env->make_string(
env, "Invalid EKP C module input", 26);
env->funcall(env, env->intern(env, "define-error"), 2,
(emacs_value[]){error_name, error_message});
/* Define functions */ /* Define functions */
defun(env, "ekp-c-init", 0, 0, Fekp_c_init, defun(env, "ekp-c-init", 0, 0, Fekp_c_init,
"Initialize EKP C module with thread pool."); "Initialize EKP C module with thread pool.");
@ -785,21 +662,42 @@ int emacs_module_init(struct emacs_runtime *runtime)
defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup, defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup,
"Cleanup EKP C module resources."); "Cleanup EKP C module resources.");
defun(env, "ekp-c-set-penalties", 4, 8, Fekp_c_set_penalties, defun(env, "ekp-c-load-hyphenator", 1, 1, Fekp_c_load_hyphenator,
"Load hyphenation dictionary from PATH.\n\
Returns hyphenator index or nil on failure.\n\n(fn PATH)");
defun(env, "ekp-c-set-spacing", 9, 9, Fekp_c_set_spacing,
"Set spacing parameters (in pixels).\n\n\
Arguments are: LWS-IDEAL LWS-STRETCH LWS-SHRINK\n\
MWS-IDEAL MWS-STRETCH MWS-SHRINK\n\
CWS-IDEAL CWS-STRETCH CWS-SHRINK\n\n\
LWS = Latin Word Space, MWS = Mixed, CWS = CJK.\n\n\
(fn LWS-I LWS-+ LWS-- MWS-I MWS-+ MWS-- CWS-I CWS-+ CWS--)");
defun(env, "ekp-c-set-penalties", 4, 6, Fekp_c_set_penalties,
"Set Knuth-Plass algorithm penalties.\n\n\ "Set Knuth-Plass algorithm penalties.\n\n\
LINE-PENALTY: base penalty per line break (default 10)\n\ LINE-PENALTY: base penalty per line break (default 10)\n\
HYPHEN-PENALTY: penalty for hyphenated breaks (default 50)\n\ HYPHEN-PENALTY: penalty for hyphenated breaks (default 50)\n\
FITNESS-PENALTY: penalty for adjacent line tightness mismatch (default 100)\n\ FITNESS-PENALTY: penalty for adjacent line tightness mismatch (default 100)\n\
LAST-LINE-RATIO: minimum fill ratio for last line (default 0.5)\n\ LAST-LINE-RATIO: minimum fill ratio for last line (default 0.5)\n\
CONSEC-HYPHEN-PENALTY: multiplier for consecutive hyphen runs (default 100)\n\ CONSEC-HYPHEN-PENALTY: multiplier for consecutive hyphen runs (default 100)\n\
LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\n\ LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\n\n\
EXTRA-STRETCH: per-line non-justify flexibility in pixels (default 0)\n\
EMERGENCY-STRETCH: fixed final-pass emergency stretch in pixels (default 0)\n\n\
(fn LINE-PENALTY HYPHEN-PENALTY FITNESS-PENALTY LAST-LINE-RATIO \ (fn LINE-PENALTY HYPHEN-PENALTY FITNESS-PENALTY LAST-LINE-RATIO \
&optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY EXTRA-STRETCH \ &optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY)");
EMERGENCY-STRETCH)");
defun(env, "ekp-c-break-with-arrays", 15, 15, Fekp_c_break_with_arrays, defun(env, "ekp-c-hyphenate", 2, 2, Fekp_c_hyphenate,
"Get hyphenation positions for WORD using HYPHENATOR-INDEX.\n\
Returns list of positions where word can be hyphenated.\n\n(fn HYPHENATOR-INDEX WORD)");
defun(env, "ekp-c-break-lines", 4, 4, Fekp_c_break_lines,
"Break STRING into lines of LINE-WIDTH pixels.\n\n\
Uses Knuth-Plass optimal line breaking with hyphenation.\n\
HYPHENATOR-INDEX: index from `ekp-c-load-hyphenator', or -1 for none\n\
MEASURE-FUNC: function that takes a string and returns pixel width\n\n\
Returns (BREAKS . TOTAL-COST) where BREAKS is list of break positions.\n\n\
(fn STRING HYPHENATOR-INDEX LINE-WIDTH MEASURE-FUNC)");
defun(env, "ekp-c-break-with-arrays", 11, 11, Fekp_c_break_with_arrays,
"Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\ "Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\
IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\ IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\
MIN-PREFIX: vector of min width prefix sums (n+1 elements)\n\ MIN-PREFIX: vector of min width prefix sums (n+1 elements)\n\
@ -811,16 +709,11 @@ HYPHEN-POS: vector of hyphenable box indices (sorted)\n\
HYPHEN-WIDTH: pixel width of hyphen character\n\ HYPHEN-WIDTH: pixel width of hyphen character\n\
LINE-WIDTH: target line width in pixels\n\ LINE-WIDTH: target line width in pixels\n\
LEAD-SPACES: vector (n+1) of space-box run widths starting at box i\n\ LEAD-SPACES: vector (n+1) of space-box run widths starting at box i\n\
TRAIL-SPACES: vector (n+1) of space-box run widths ending at box k-1\n\ TRAIL-SPACES: vector (n+1) of space-box run widths ending at box k-1\n\n\
FORBIDDEN-POS: vector of gap indices where breaking is forbidden (sorted)\n\
TAIL-PROTRUDES: vector (n+1) of right-edge protrusion pixels per gap\n\
HYPHEN-PROTRUDE: protrusion pixels for the soft hyphen\n\
FIRST-LINE-WIDTH: width of line 0 (first-line indent); <=0 = LINE-WIDTH\n\n\
Returns (BREAKS . TOTAL-COST) where BREAKS is list of box indices.\n\ Returns (BREAKS . TOTAL-COST) where BREAKS is list of box indices.\n\
This API ensures C uses Elisp's font-dependent measurements.\n\n\ This API ensures C uses Elisp's font-dependent measurements.\n\n\
(fn IDEAL-PREFIX MIN-PREFIX MAX-PREFIX GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES \ (fn IDEAL-PREFIX MIN-PREFIX MAX-PREFIX GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES \
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS \ HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES)");
TAIL-PROTRUDES HYPHEN-PROTRUDE FIRST-LINE-WIDTH)");
defun(env, "ekp-c-version", 0, 0, Fekp_c_version, defun(env, "ekp-c-version", 0, 0, Fekp_c_version,
"Return EKP C module version string."); "Return EKP C module version string.");
@ -830,8 +723,10 @@ TAIL-PROTRUDES HYPHEN-PROTRUDE FIRST-LINE-WIDTH)");
defun(env, "ekp-c-break-batch", 1, 1, Fekp_c_break_batch, defun(env, "ekp-c-break-batch", 1, 1, Fekp_c_break_batch,
"Break multiple paragraphs in parallel.\n\n\ "Break multiple paragraphs in parallel.\n\n\
PARAGRAPHS: vector of paragraph data, each element a vector of the\n\ PARAGRAPHS: vector of paragraph data, each element is a vector of 11 items:\n\
same 15 items `ekp-c-break-with-arrays' takes, in the same order.\n\n\ [ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks\n\
glue-stretches hyphen-positions hyphen-width line-width\n\
lead-spaces trail-spaces]\n\n\
Returns vector of (BREAKS . COST) for each paragraph.\n\ Returns vector of (BREAKS . COST) for each paragraph.\n\
This is the high-performance API for multi-paragraph processing.\n\n\ This is the high-performance API for multi-paragraph processing.\n\n\
(fn PARAGRAPHS)"); (fn PARAGRAPHS)");

Some files were not shown because too many files have changed in this diff Show More