Compare commits
53 Commits
kp-overhau
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5785405495 | ||
|
|
711b0b1abe | ||
|
|
a3d20aa87e | ||
|
|
97cb6a6e35 | ||
|
|
d216c6b761 | ||
|
|
3d3dda6688 | ||
|
|
cfa7c18beb | ||
|
|
5f78cae0b1 | ||
|
|
01001b9008 | ||
|
|
5efd4d113b | ||
|
|
db6e6b98ee | ||
|
|
19b8c9050e | ||
|
|
60b299b3f4 | ||
|
|
c47abf7529 | ||
|
|
dbb3809d7c | ||
|
|
c93786570b | ||
|
|
6a8c7e04f5 | ||
|
|
183f256020 | ||
|
|
888a4019f4 | ||
|
|
e823d89a4a | ||
|
|
665164911f | ||
|
|
83352c4571 | ||
|
|
fd1210e557 | ||
|
|
29cef97ef8 | ||
|
|
448198bdb9 | ||
|
|
4dee06ec47 | ||
|
|
81d32c7052 | ||
|
|
4ba97deec7 | ||
|
|
03096facc5 | ||
|
|
74a780ce95 | ||
|
|
3fc05c1405 | ||
|
|
bed359d439 | ||
|
|
bb7b21eb92 | ||
|
|
bc4907e1fc | ||
|
|
f0c7927644 | ||
|
|
a12a3c1191 | ||
|
|
ede518f10f | ||
|
|
9dcaeb0ba3 | ||
|
|
c7a89df6ce | ||
|
|
5233baeea0 | ||
|
|
dc2f6da846 | ||
|
|
c4d7fdf8bb | ||
|
|
c703ce0226 | ||
|
|
4d9a018fbf | ||
|
|
720b1cd0e4 | ||
|
|
f6aa64b3b1 | ||
|
|
57a3abe853 | ||
|
|
ea96a6dc96 | ||
|
|
64eb2f38d5 | ||
|
|
33981a647e | ||
|
|
95ed2b32d1 | ||
|
|
d3972fdc26 | ||
|
|
9afe491e1a |
171
.github/workflows/ci.yml
vendored
Normal file
171
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,171 @@
|
||||
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
2
.gitignore
vendored
@ -4,3 +4,5 @@ archive
|
||||
*.dll
|
||||
*.o
|
||||
*.elc
|
||||
AGENTS.md
|
||||
!AGENTS.md
|
||||
|
||||
@ -1,4 +1,122 @@
|
||||
# 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
|
||||
- See `.phrase/phases/phase-doc-improvement-20260125/change_log.md`
|
||||
|
||||
@ -1,3 +1,31 @@
|
||||
# Known Issues
|
||||
|
||||
(No open issues)
|
||||
- issue001 [x] [Incomplete cache signatures reuse stale layout results.](../phases/phase-kp-overhaul-20260726/issue_repository_audit_20260728.md)
|
||||
- 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 60–70 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)
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
# 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
|
||||
|
||||
- **Add**: `DEVELOPER.md` and `DEVELOPER_ZH.md`
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# Tasks: Documentation Improvement
|
||||
|
||||
- task001 [ ] Create `DEVELOPER.md` with extracted technical content from `readme.md`
|
||||
- task002 [ ] Create `DEVELOPER_ZH.md` with extracted technical content from `readme_zh.md`
|
||||
- task003 [ ] Enhance `DEVELOPER.md` with detailed Elisp API and C Module internals
|
||||
- task004 [ ] Enhance `DEVELOPER_ZH.md` with detailed Elisp API and C Module internals
|
||||
- task005 [ ] Refine `readme.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)
|
||||
- task001 [x] Create `DEVELOPER.md` with extracted technical content from `readme.md`
|
||||
- task002 [x] 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
|
||||
- task004 [x] 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)
|
||||
- task006 [x] Refine `readme_zh.md` to be user-focused (remove internal details, add links to Dev docs)
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# KP 算法系统性优化 — 进度交接文档
|
||||
|
||||
> **2026-07-28 superseded note:** 本文的“未完成事项”保留为 2026-07-26
|
||||
> 历史快照;后续审计、整改和验证状态以
|
||||
> `task_repository_audit_20260728.md` 及 `.phrase/docs/ISSUES.md`
|
||||
> 为准。
|
||||
>
|
||||
> 阶段:phase-kp-overhaul-20260726
|
||||
> 状态:**已完成并提交**——分支 `kp-overhaul`(基于 main@11437cb),
|
||||
> 合并到 main:`git checkout main && git merge kp-overhaul`
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
# 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.
|
||||
@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
@ -0,0 +1,39 @@
|
||||
# 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 60–70 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.
|
||||
@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
@ -0,0 +1,175 @@
|
||||
# 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.
|
||||
@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
@ -0,0 +1,74 @@
|
||||
# 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
|
||||
33–85 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.
|
||||
@ -0,0 +1,53 @@
|
||||
# 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.
|
||||
@ -0,0 +1,91 @@
|
||||
# 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.
|
||||
@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
@ -0,0 +1,39 @@
|
||||
# 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 1k–8k 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.
|
||||
@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
@ -0,0 +1,35 @@
|
||||
# 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.158–1.326 ms for C and
|
||||
1.429–1.438 ms for pure Elisp; hard-boundary p99 is 1.251–1.363 ms and
|
||||
1.457–1.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.
|
||||
@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
@ -0,0 +1,45 @@
|
||||
# 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.
|
||||
@ -0,0 +1,43 @@
|
||||
# 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.
|
||||
@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
@ -0,0 +1,76 @@
|
||||
# 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 33–85 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.
|
||||
@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
@ -0,0 +1,31 @@
|
||||
# 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.
|
||||
@ -0,0 +1,286 @@
|
||||
# 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 1–64px 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 1–64px 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 1–64px 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.
|
||||
@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
@ -0,0 +1,23 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,19 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,56 @@
|
||||
# Issue: C Resize Latency 2026-07-29
|
||||
|
||||
## issue020 [x] C-backed resize reflow still takes roughly 60–70 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 60–70 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).
|
||||
@ -0,0 +1,44 @@
|
||||
# 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)
|
||||
@ -0,0 +1,20 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,20 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,22 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,119 @@
|
||||
# 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`
|
||||
@ -0,0 +1,20 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,132 @@
|
||||
# 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 1–64px 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`.
|
||||
@ -0,0 +1,97 @@
|
||||
# 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.158–1.326 ms for C and 1.429–1.438 ms for pure
|
||||
Elisp. Hard-boundary p99 is 1.251–1.363 ms and 1.457–1.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).
|
||||
@ -0,0 +1,50 @@
|
||||
# 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).
|
||||
@ -0,0 +1,54 @@
|
||||
# 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).
|
||||
@ -0,0 +1,69 @@
|
||||
# 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`).
|
||||
@ -0,0 +1,81 @@
|
||||
# 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).
|
||||
@ -0,0 +1,273 @@
|
||||
# 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; 1k–8k 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`.
|
||||
@ -0,0 +1,97 @@
|
||||
# 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 33–85 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).
|
||||
@ -0,0 +1,25 @@
|
||||
# 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`.
|
||||
@ -0,0 +1,72 @@
|
||||
# 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).
|
||||
@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
@ -0,0 +1,31 @@
|
||||
# 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.
|
||||
@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
@ -0,0 +1,42 @@
|
||||
# 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.
|
||||
@ -0,0 +1,42 @@
|
||||
# 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.
|
||||
@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
@ -0,0 +1,27 @@
|
||||
# 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.
|
||||
@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
@ -0,0 +1,411 @@
|
||||
# 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.
|
||||
@ -0,0 +1,244 @@
|
||||
# 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 1–64px 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.
|
||||
@ -0,0 +1,92 @@
|
||||
# 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
|
||||
task037–task042. 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.
|
||||
@ -0,0 +1,926 @@
|
||||
# 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 1k–8k 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 1–64px 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 1–64px 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.158–1.326 ms for C
|
||||
and 1.429–1.438 ms for pure Elisp; hard-boundary p99 is
|
||||
1.251–1.363 ms and 1.457–1.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 60–70 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.
|
||||
@ -0,0 +1,408 @@
|
||||
# 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.
|
||||
82
.phrase/phases/phase-p1p2-20260726/NOTES.md
Normal file
82
.phrase/phases/phase-p1p2-20260726/NOTES.md
Normal file
@ -0,0 +1,82 @@
|
||||
# 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
|
||||
295
CHANGELOG.md
Normal file
295
CHANGELOG.md
Normal file
@ -0,0 +1,295 @@
|
||||
# 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.158–1.326 ms for C and 1.429–1.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,000–8,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.
|
||||
74
CONTRIBUTING.md
Normal file
74
CONTRIBUTING.md
Normal file
@ -0,0 +1,74 @@
|
||||
# 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
Normal file
674
COPYING
Normal file
@ -0,0 +1,674 @@
|
||||
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>.
|
||||
367
DEVELOPER.md
367
DEVELOPER.md
@ -4,6 +4,9 @@ This document describes the internal architecture, algorithms and APIs of
|
||||
`emacs-kp`, as implemented. It is intended for contributors and advanced
|
||||
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
|
||||
|
||||
A justification call flows through five stages:
|
||||
@ -55,17 +58,26 @@ 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) |
|
||||
| `trail-spaces` | `trail-spaces[k]` = width of the space-box run ending at box k−1 |
|
||||
| `glue-params` | plist snapshot of the nine spacing values at creation time |
|
||||
| `dp-cache` | hash: line-width → dp-result plist |
|
||||
| `dp-cache` | equal-keyed hash: complete DP signature → dp-result plist |
|
||||
|
||||
The paragraph cache (`ekp--para-cache`) is keyed with `equal` on a
|
||||
structured key — string content, printed text-property intervals,
|
||||
detected fonts, the hyphenation language (`ekp-latin-lang`), and
|
||||
either the nine explicit spacing values or the symbol `auto`.
|
||||
either the nine explicit spacing values or the automatic CJK stretch
|
||||
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
|
||||
the previous `sxhash`-integer scheme). The cache is flushed when it
|
||||
exceeds `ekp-para-cache-limit`. A one-entry fast path
|
||||
(`ekp--last-para`, checked by string `eq` + language) covers the many
|
||||
same-string lookups inside one justification call.
|
||||
(`ekp--last-para`) bypasses only the hash lookup: it compares the same
|
||||
complete structural key before reuse. In-place changes to layout-relevant
|
||||
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
|
||||
|
||||
@ -88,6 +100,8 @@ 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
|
||||
renderer therefore agree exactly, and every justified line renders at
|
||||
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
|
||||
|
||||
@ -107,10 +121,12 @@ badness = min(10000, 100·|adjustment/flexibility|³)
|
||||
```
|
||||
|
||||
Fitness classes (tight/decent/loose/very-loose) follow the TeX ratio
|
||||
thresholds. Special cases: single-box lines use flexibility 1 and
|
||||
fitness decent; the last line pays `(line-penalty + short-badness)²`
|
||||
where `short-badness = last-line-short-penalty × (1 − fill)` when the
|
||||
fill ratio is below `ekp-last-line-min-ratio`.
|
||||
thresholds. Special cases: single-box lines use flexibility 1 in the
|
||||
strict pass; the final pass uses the same finite emergency stretch as
|
||||
ordinary underfull lines. The last line pays
|
||||
`(line-penalty + short-badness)²` where
|
||||
`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
|
||||
as `+p²` (no negative/flagged penalties), there is no `q`/looseness in
|
||||
@ -121,12 +137,22 @@ the main pass (see §6), and adjacent-fitness is a flat constant.
|
||||
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.
|
||||
A strict pass runs first; if the paragraph end is unreachable, a second
|
||||
pass additionally allows **emergency breaks** — single-box lines with
|
||||
demerits `(line-penalty + 10000)² + rest²`, at least as bad as any
|
||||
regular line. This guarantees, by induction over positions, that every
|
||||
input produces output (regression: narrow CJK used to return an empty
|
||||
string), while the common case pays nothing and keeps pure K-P
|
||||
optimality. Both engines implement the identical strategy.
|
||||
pass adds a finite background emergency stretch to ordinary underfull
|
||||
candidates and still scores them through the same adjustment ratio,
|
||||
badness, fitness, and demerits used by the strict pass. That keeps
|
||||
underfull final-pass choices inside the global K-P DP instead of forcing
|
||||
them through a separate fixed-cost path.
|
||||
|
||||
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
|
||||
|
||||
@ -139,14 +165,213 @@ optimality. Both engines implement the identical strategy.
|
||||
- Last lines are ragged-right (ideal glues + trailing filler);
|
||||
single-box lines get a trailing filler clamped at ≥ 0.
|
||||
|
||||
`ekp--pixel-justify` then strips leading space boxes (except on the
|
||||
first line — indentation) and trailing space boxes, and appends a
|
||||
hyphen — propertized like the word it breaks — where a line ends at a
|
||||
hyphenation point. Stripped widths are *not* redistributed: the DP
|
||||
already excluded them (§3).
|
||||
`ekp-layout-plan` is the representation boundary between layout and
|
||||
rendering. It combines the paragraph boxes/source offsets, DP breaks,
|
||||
per-line glue targets, indentation, stripped edges, and discretionary
|
||||
hyphen decisions into `ekp-layout-plan`, `ekp-layout-line`, and
|
||||
`ekp-layout-gap` records. The plan contains no buffer positions or display
|
||||
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.
|
||||
|
||||
Glues become `(space :width (N))` display properties, so justification
|
||||
is pixel-exact in GUI Emacs and column-exact in batch/tty.
|
||||
Cached semantic plans are immutable to the cache owner. A cache hit returns
|
||||
a consumer-owned copy of every plan-owned mutable payload: source string,
|
||||
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 4–8. 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
|
||||
|
||||
@ -159,33 +384,60 @@ engines never disagree.
|
||||
|
||||
## 7. C Module Integration
|
||||
|
||||
The C module (`ekp_c/`, version 1.1) runs only stage ④. Elisp remains
|
||||
The C module (`ekp_c/`, version 1.6) runs only stage ④. Elisp remains
|
||||
the source of truth for all font-dependent data.
|
||||
|
||||
- `ekp-c-break-with-arrays` (11 args): the para's prefix arrays, glue
|
||||
arrays, hyphen data, line width and the two space-run arrays.
|
||||
Returns `(breaks . cost)`.
|
||||
- `ekp-c-break-batch`: a vector of 11-element vectors, processed in
|
||||
- `ekp-c-break-with-arrays` (15 args): the para's prefix arrays, glue
|
||||
arrays, hyphen data, line width, the two space-run arrays, the
|
||||
forbidden/protrusion arrays and the first-line width. Returns
|
||||
`(breaks . cost)`.
|
||||
- `ekp-c-break-batch`: a vector of 15-element vectors, processed in
|
||||
parallel by a pthread pool — one task per paragraph (that is the
|
||||
correct granularity; the DP itself is sequential by nature).
|
||||
- `ekp-c-set-penalties` (4–6 args): called by `ekp--c-sync-params`
|
||||
correct granularity; the DP itself is sequential by nature). The
|
||||
pool is created lazily on the first multi-paragraph batch and sized
|
||||
to the machine's cores; a full queue blocks the submitter rather
|
||||
than dropping the task.
|
||||
- `ekp-c-set-penalties` (4–8 args): called by `ekp--c-sync-params`
|
||||
before *every* C entry, so `ekp-line-penalty` & friends always take
|
||||
effect (regression: they were never synced before).
|
||||
- `ekp-c-module-load` refuses modules older than
|
||||
`ekp-c-module-required-version` and falls back to Elisp, preventing
|
||||
arity mismatches after upgrades.
|
||||
|
||||
Any C failure (NULL result) silently falls back to the Elisp engine.
|
||||
The two engines are verified to produce byte-identical output by
|
||||
`ekp-test-c-parity-simple` / `ekp-test-c-parity-files`.
|
||||
An unavailable module, an incompatible module version, a nil whole C result,
|
||||
or a nil per-item/break result falls back to the Elisp engine. Invalid
|
||||
direct API input signals `ekp-c-invalid-input`. Any non-nil malformed
|
||||
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.
|
||||
|
||||
`ekp-c-break-lines` (C-side tokenization via `ekp_paragraph.c` and
|
||||
`ekp_hyphen.c`) is an experimental, self-contained path that ekp.el
|
||||
does not use; see `ekp_c/README.md`.
|
||||
### Future direction: a paragraph-handle API
|
||||
|
||||
Each `ekp-c-break-with-arrays` call re-marshals the paragraph's
|
||||
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)
|
||||
|
||||
Liang's pattern algorithm, Pyphen-compatible:
|
||||
Liang's ordinary pattern algorithm:
|
||||
|
||||
- `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
|
||||
@ -193,6 +445,16 @@ Liang's pattern algorithm, Pyphen-compatible:
|
||||
- `ekp-hyphen-create LANG` resolves exact codes, then progressively
|
||||
shorter prefixes (`"de_CH" → "de"`).
|
||||
- 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
|
||||
`^[left-punct]* (latin-word) [right-punct]*$` so that punctuation-
|
||||
@ -202,12 +464,34 @@ punctuation stays glued to the first/last syllable box.
|
||||
## 9. Testing & Benchmarks
|
||||
|
||||
```bash
|
||||
tests/run-tests.sh [emacs] # 36 ERT tests, batch-safe
|
||||
tests/run-tests.sh [emacs] # batch-safe ERT suite
|
||||
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 '(progn (require (quote ekp)) (ekp-c-module-load))' \
|
||||
-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
|
||||
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
|
||||
@ -245,9 +529,12 @@ ekp.el Core: para struct, caching, DP (1D + looseness),
|
||||
ekp-utils.el Tokenizer (boxes, kinsoku), font detection with
|
||||
batch/tty fallbacks, C module loading
|
||||
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)
|
||||
dictionaries/ Hunspell hyphenation patterns (from Pyphen)
|
||||
tests/ ekp-tests.el (ERT), ekp-bench.el, ekp-demo.el,
|
||||
sample texts, run-tests.sh
|
||||
archive/ Historical prototypes; not loaded, kept for reference
|
||||
dictionaries/ Hunspell hyphenation patterns (from LibreOffice)
|
||||
tests/ ekp-tests.el, ekp-buffer-tests.el (ERT),
|
||||
ekp-fuzz.el (parity fuzz), ekp-bench.el,
|
||||
ekp-demo.el, ekp-showcase.el, sample texts,
|
||||
run-tests.sh
|
||||
```
|
||||
|
||||
295
DEVELOPER_ZH.md
295
DEVELOPER_ZH.md
@ -2,6 +2,9 @@
|
||||
|
||||
本文档描述 `emacs-kp` 的实际内部架构、算法与 API,面向贡献者和高级用户。
|
||||
|
||||
当前仓库健康度与后续工作的优先级见
|
||||
[2026-07-28 系统审计](./Docs/REPOSITORY_AUDIT_20260728.md)。
|
||||
|
||||
## 1. 处理管线
|
||||
|
||||
一次排版调用经过五个阶段:
|
||||
@ -52,14 +55,20 @@ DP 和渲染需要的一切,每段只算一次:
|
||||
| `lead-spaces` | `lead-spaces[i]` = 从盒 i 开始的连续空格盒总宽;下标 0 强制为 0(首行缩进保留) |
|
||||
| `trail-spaces` | `trail-spaces[k]` = 到盒 k−1 结束的连续空格盒总宽 |
|
||||
| `glue-params` | 创建时九个间距值的 plist 快照 |
|
||||
| `dp-cache` | 哈希:行宽 → dp-result plist |
|
||||
| `dp-cache` | 以 `equal` 比较的哈希:完整 DP 签名 → dp-result plist |
|
||||
|
||||
段落缓存(`ekp--para-cache`)以 `equal` 比较结构化 key——字符串内容、
|
||||
文本属性区间的打印形式、检测字体、断词语言(`ekp-latin-lang`)、九个
|
||||
显式间距值(或符号 `auto`)。结构化 key 使哈希碰撞无害(旧的 `sxhash`
|
||||
整数方案理论上可能串段)。超过 `ekp-para-cache-limit` 时整体清空。
|
||||
单条快路径(`ekp--last-para`,按字符串 `eq` + 语言校验)覆盖同一次
|
||||
排版内的大量同字符串查询。
|
||||
显式间距值或自动 CJK stretch 默认值。其他自动值由字体测量派生,已
|
||||
由字体与显示上下文字段表达。结构化 key 使哈希碰撞无害(旧的
|
||||
`sxhash` 整数方案理论上可能串段)。超过 `ekp-para-cache-limit` 时
|
||||
整体清空。单条快路径(`ekp--last-para`)只绕过哈希 lookup,复用前仍
|
||||
比较同一个完整结构 key。因此排版相关 text property 的原地修改会让
|
||||
两条路径都 miss,并得到与 fresh paragraph 相同的结果。
|
||||
|
||||
DP 签名独立于段落 key,包含行宽、looseness 与全部六个运行时代价参数。
|
||||
参数变化会选择新结果,无需丢弃与宽度无关的段落数据;结构化 `equal`
|
||||
比较也让非零 looseness 签名能够正常命中缓存。
|
||||
|
||||
### dp-result
|
||||
|
||||
@ -80,6 +89,8 @@ width = raw − space-w (若盒 k−1 处断词,再加连字符宽)
|
||||
理想/最小/最大三个值均 O(1) 得出。行边缘的空格盒串被排除,因为渲染层
|
||||
会剥离它们;DP 与渲染层因此严格一致,每一行的渲染宽度精确等于目标宽
|
||||
(测试 `ekp-test-justify-line-width-invariant`)。
|
||||
`ekp--line-stripped-space-pixel` 统一拥有 1D/loose DP、C 结果重建与
|
||||
渲染层使用的这一排除规则。
|
||||
|
||||
## 4. Knuth-Plass 动态规划
|
||||
|
||||
@ -98,7 +109,8 @@ badness = min(10000, 100·|adjustment/flexibility|³)
|
||||
```
|
||||
|
||||
松紧等级(tight/decent/loose/very-loose)沿用 TeX 的比例阈值。特殊情
|
||||
况:单盒行 flexibility 固定为 1、fitness 为 decent;末行代价为
|
||||
况:严格遍的单盒行 flexibility 固定为 1;最终遍使用与普通欠宽行相同
|
||||
的有限 emergency stretch。末行代价为
|
||||
`(line-penalty + 短行badness)²`,填充率低于 `ekp-last-line-min-ratio`
|
||||
时 `短行badness = last-line-short-penalty × (1 − 填充率)`。
|
||||
|
||||
@ -110,10 +122,17 @@ penalty/flagged 断点),主流程无 `q`/looseness(见 §6),相邻松紧惩
|
||||
|
||||
某些输入不存在合法排版:比行宽更宽的不可断盒子,或无法伸展到目标宽
|
||||
的刚性(全 `nws`)区段。先跑严格遍;若段尾不可达,第二遍额外允许
|
||||
**紧急断行**——demerits 为 `(line-penalty + 10000)² + rest²` 的单盒行,
|
||||
不低于任何常规行的代价。由位置归纳可证:任何输入必有输出(回归:窄
|
||||
栏 CJK 曾整段返回空串),常规输入不付任何代价、保持纯 K-P 最优。两个
|
||||
引擎实现完全相同的策略。
|
||||
为普通欠宽候选加入有限 background emergency stretch,并继续走同一套
|
||||
adjustment ratio、badness、fitness 和 demerits。这样最终遍的欠宽选择
|
||||
仍由全局 K-P DP 比较,不会被塞进另一条固定代价路径。
|
||||
|
||||
另外,最终遍实现 TeX 的 `artificial_demerits` 可达性保护。如果某个
|
||||
超宽候选将使一个断点的最后活动路径消失,且该断点没有任何未超宽候选
|
||||
存活,就以 tight fitness 和零增量 demerits 安装最佳暂存路径。这不是
|
||||
hard atom 专用的评分捷径:只要正常活动路径还在,它就不参与竞争,也不
|
||||
检查中文、单位或 token 类别。因此任何输入仍会得到覆盖完整源文本的
|
||||
plan(回归:窄栏 CJK 曾整段返回空结果),而普通欠宽行仍按正常 K-P 代价
|
||||
竞争。Elisp 1D、looseness/parshape 与 C 引擎实现相同语义。
|
||||
|
||||
## 5. 渲染
|
||||
|
||||
@ -125,12 +144,174 @@ penalty/flagged 断点),主流程无 `q`/looseness(见 §6),相邻松紧惩
|
||||
≥ 0。
|
||||
- 末行右侧不齐(理想 glue + 尾部填充);单盒行的尾部填充钳制 ≥ 0。
|
||||
|
||||
`ekp--pixel-justify` 随后剥离行首空格盒(首行除外——缩进)与行尾空格
|
||||
盒,在断词处附加连字符(继承所断单词的文本属性)。剥离的宽度**不再**
|
||||
重新分配:DP 已经排除了它们(§3)。
|
||||
`ekp-layout-plan` 是布局决策与渲染方式之间的表示边界。它把段落盒子
|
||||
及其原文 offset、DP 断点、逐行 glue 目标、缩进、边缘剥离和可选断词
|
||||
决策组合成 `ekp-layout-plan`、`ekp-layout-line` 和 `ekp-layout-gap`
|
||||
记录。plan 不包含 buffer 位置或具体显示机制。
|
||||
`ekp-render-layout-string` 用它服务现有字符串 API;buffer 集成可复用
|
||||
完全相同的决策,无需重新运行或重新解释 KP 算法。
|
||||
|
||||
Glue 渲染为 `(space :width (N))` display 属性,GUI 下像素级精确,
|
||||
batch/tty 下按字符列精确。
|
||||
缓存的 semantic plan 对缓存 owner 不可变。缓存命中会返回 consumer
|
||||
自有副本,覆盖所有 plan 自有的可变 payload:源字符串、context 树、盒
|
||||
向量与盒字符串、源 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` 4–8 参。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
|
||||
|
||||
@ -142,34 +323,65 @@ batch/tty 下按字符列精确。
|
||||
|
||||
## 7. C 模块集成
|
||||
|
||||
C 模块(`ekp_c/`,版本 1.1)只执行阶段 ④。所有字体相关数据以 Elisp
|
||||
C 模块(`ekp_c/`,版本 1.6)只执行阶段 ④。所有字体相关数据以 Elisp
|
||||
为唯一事实来源。
|
||||
|
||||
- `ekp-c-break-with-arrays`(11 参数):para 的前缀数组、glue 数组、
|
||||
断词数据、行宽和两个空格串数组。返回 `(breaks . cost)`。
|
||||
- `ekp-c-break-batch`:11 元素向量的向量,由 pthread 线程池并行处理
|
||||
——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。
|
||||
- `ekp-c-set-penalties`(4–6 参数):`ekp--c-sync-params` 在**每次**
|
||||
- `ekp-c-break-with-arrays`(15 参数):para 的前缀数组、glue 数组、
|
||||
断词数据、行宽、两个空格串数组、禁则/悬挂数组和首行宽度。返回
|
||||
`(breaks . cost)`。
|
||||
- `ekp-c-break-batch`:15 元素向量的向量,由 pthread 线程池并行处理
|
||||
——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。线程池在
|
||||
首次多段落 batch 时惰性创建,按机器核心数定大小;队列满时提交方
|
||||
阻塞等待而非丢弃任务。
|
||||
- `ekp-c-set-penalties`(4–8 参数):`ekp--c-sync-params` 在**每次**
|
||||
进入 C 之前调用,保证 `ekp-line-penalty` 等变量始终生效(回归:此
|
||||
前从未同步)。
|
||||
- `ekp-c-module-load` 拒绝低于 `ekp-c-module-required-version` 的模块
|
||||
并回落到 Elisp,避免升级后的参数数量不匹配。
|
||||
|
||||
C 端任何失败(返回 NULL)都会静默回落到 Elisp 引擎。两引擎输出逐字
|
||||
节一致,由 `ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 验证。
|
||||
模块不可用、ABI 版本不兼容、整批 C 结果为 nil 或单项/断点结果为
|
||||
nil 时回落到 Elisp。直接 API 的非法输入 signal
|
||||
`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 验证。
|
||||
|
||||
`ekp-c-break-lines`(经 `ekp_paragraph.c`、`ekp_hyphen.c` 的 C 端自行
|
||||
分词路径)是实验性的独立路径,ekp.el 不使用;见 `ekp_c/README.md`。
|
||||
### 未来方向:段落句柄 API
|
||||
|
||||
每次 `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)
|
||||
|
||||
Liang 模式算法,兼容 Pyphen:
|
||||
普通 Liang 模式算法:
|
||||
|
||||
- `dictionaries/hyph_*.dic` 首次使用时编译为模式哈希并按路径缓存。
|
||||
文件可为 UTF-8 或 ISO-8859(Emacs 自动检测;由
|
||||
`ekp-test-hyphen-de-iso8859-dict` 验证)。
|
||||
- `ekp-hyphen-create LANG` 先精确匹配,再逐级缩短(`"de_CH" → "de"`)。
|
||||
- 断点两侧默认至少保留 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«`)仍可断词;标点粘在首/末音节盒上。
|
||||
@ -177,12 +389,31 @@ Liang 模式算法,兼容 Pyphen:
|
||||
## 9. 测试与基准
|
||||
|
||||
```bash
|
||||
tests/run-tests.sh [emacs] # 36 个 ERT 测试,batch 可跑
|
||||
tests/run-tests.sh [emacs] # batch 可跑的 ERT 测试集
|
||||
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 '(progn (require (quote ekp)) (ekp-c-module-load))' \
|
||||
-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 一致性、
|
||||
参数持久化/同步回归。
|
||||
@ -216,9 +447,11 @@ ekp.el 核心:para 结构、缓存、DP(1D + looseness)、
|
||||
ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体
|
||||
检测、C 模块加载
|
||||
ekp-hyphen.el Liang 断词 + 词典注册
|
||||
ekp-buffer.el 纯文本属性 buffer/region 投影、同步实时流动、窗口
|
||||
lifecycle、复制过滤与诊断
|
||||
ekp_c/ C 动态模块(见 ekp_c/README.md)
|
||||
dictionaries/ Hunspell 断词模式(来自 Pyphen)
|
||||
tests/ ekp-tests.el(ERT)、ekp-bench.el、ekp-demo.el、
|
||||
示例文本、run-tests.sh
|
||||
archive/ 历史原型;不参与加载,仅作参考
|
||||
dictionaries/ Hunspell 断词模式(来自 LibreOffice)
|
||||
tests/ ekp-tests.el、ekp-buffer-tests.el(ERT)、
|
||||
ekp-fuzz.el(一致性 fuzz)、ekp-bench.el、
|
||||
ekp-demo.el、ekp-showcase.el、示例文本、run-tests.sh
|
||||
```
|
||||
|
||||
56
Docs/RELEASING.md
Normal file
56
Docs/RELEASING.md
Normal file
@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
570
Docs/REPOSITORY_AUDIT_20260728.md
Normal file
570
Docs/REPOSITORY_AUDIT_20260728.md
Normal file
@ -0,0 +1,570 @@
|
||||
# emacs-kp 仓库系统审计
|
||||
|
||||
> 审计日期:2026-07-28
|
||||
> 审计基线:`main@29cef97`(本地 tag `v1.0.0`)
|
||||
> 范围:Elisp 核心、C 动态模块、buffer/region 集成、测试、性能、兼容性、文档、发布与扩展边界
|
||||
> 性质:问题盘点与方向判断,不包含运行代码修复
|
||||
|
||||
## 后续状态
|
||||
|
||||
- `task002` 已在审计后的工作树中解决 P1-01:DP key 现在包含行宽、
|
||||
looseness 与全部六个运行时代价参数,并按结构相等比较。
|
||||
- `task003` 已继续解决 P2-01:auto-CWS 默认值现在同时进入段落哈希键
|
||||
与最近段落快路径;`issue001` 的两个缓存缺陷均已闭环。
|
||||
- `task004` 已解决 P1-02:保存从逻辑副本序列化,文件系统错误、编码
|
||||
错误与用户中断均不再改变显示 buffer;`issue002` 已闭环。
|
||||
- `task005` 已解决 P1-03/P2-06:copy filter 可组合并恢复,integration
|
||||
生命周期由 auto mode 与实际 justified span 共同决定;`issue003`
|
||||
已闭环。
|
||||
- `task006` 已解决 P1-04:测试 fixture 完整隔离配置,parshape 用例
|
||||
经过公开分派路径;CI、可复现乱序和逐例全新进程入口共同阻断顺序
|
||||
假绿,`issue004` 已闭环。
|
||||
- `task007` 已解决 P2-05:GUI 矩阵共享结构化结果与报告断言边界,
|
||||
任一失败行在 batch 中退出 1;强制失败负控 2/2,真实 GUI 七场景
|
||||
7/7,`issue007` 已闭环。
|
||||
- `task008` 已解决 P2-02:C 1.6 在提取前验证单段/批量完整 schema,
|
||||
非法调用统一 signal `ekp-c-invalid-input`,有效 int32 输入使用
|
||||
int64 中间量;模块 signal 不再被 Elisp 静默吞掉。
|
||||
- `task009` 已解决 P2-03:默认构建改为 portable,native/debug/
|
||||
sanitize 需显式选择;交互命令以 argv 和 `default-directory` 启动
|
||||
make,含空格路径实测通过。`issue005` 已闭环。
|
||||
- `task010` 已解决 P2-07 的仓库内边界:CI action 与 package-lint
|
||||
固定到完整提交,Windows 运行 Elisp 基线,`.phrase` 改为可追踪事实
|
||||
来源,release gate 与远端/产物 checklist 分责。未执行 push、tag
|
||||
变更或发布。
|
||||
- `task011` 已解决 P2-04:49 份词典固定来源路径与 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 入口 | PASS,94/94 | Emacs 30.2,加载 C 1.5 |
|
||||
| C/Elisp fuzz | PASS,300/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 像素矩阵 | PASS,7/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-hyphen:Liang pattern 编译与断词
|
||||
├─ ekp.el:ekp-para、前缀数组、缓存
|
||||
├─ DP
|
||||
│ ├─ Elisp 1D / 2D
|
||||
│ └─ C 1D(15 字段边界,批量按段落并行)
|
||||
└─ renderer:glue、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 使用动态绑定恢复全部
|
||||
> tunable;parshape 用例绑定真实 `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 范围做整体验证,非法输入使用专用 condition;DP 中间
|
||||
> 行宽/前缀差/剩余空间改为 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 arithmetic,API 边缘再收窄。
|
||||
- 增加 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 gate:commit/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
|
||||
> sentinel;tokenizer 与密集插入改为片段单次拼接。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/94;fuzz 300/300;GUI 数值矩阵 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 开始,每次只闭环一个原子问题。
|
||||
37
dictionaries/LICENSES.md
Normal file
37
dictionaries/LICENSES.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
54
dictionaries/MANIFEST.tsv
Normal file
54
dictionaries/MANIFEST.tsv
Normal file
@ -0,0 +1,54 @@
|
||||
# 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.
|
@ -1,116 +0,0 @@
|
||||
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
Normal file → Executable file
116
dictionaries/update.sh
Normal file → Executable file
@ -1,7 +1,109 @@
|
||||
git clone https://git.libreoffice.org/dictionaries libreoffice-dictionaries
|
||||
cd libreoffice-dictionaries
|
||||
git pull
|
||||
cd ..
|
||||
find libreoffice-dictionaries -name "hyph_*\.dic" | xargs -I '{}' cp '{}' .
|
||||
sed -i 's/\r$//' *.dic
|
||||
rename -- -Latn _Latn *-Latn.dic
|
||||
#!/bin/sh
|
||||
# Verify or export the pinned emacs-kp dictionary bundle.
|
||||
|
||||
set -eu
|
||||
|
||||
UPSTREAM_URL=https://github.com/LibreOffice/dictionaries.git
|
||||
UPSTREAM_COMMIT=8fb8e794237cff49ec212023f96bcdb7d3fbf56c
|
||||
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"
|
||||
|
||||
2519
ekp-buffer.el
Normal file
2519
ekp-buffer.el
Normal file
File diff suppressed because it is too large
Load Diff
135
ekp-hyphen.el
135
ekp-hyphen.el
@ -1,9 +1,24 @@
|
||||
;;; ekp-hyphen.el --- Liang hyphenation algorithm -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024
|
||||
;; Author: emacs-kp contributors
|
||||
;; Copyright (C) 2024-2026 Kinney Zhang
|
||||
|
||||
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
|
||||
;; 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:
|
||||
|
||||
@ -33,33 +48,54 @@ LEFT/RIGHT: minimum chars before first / after last break."
|
||||
|
||||
;;; 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)
|
||||
"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)
|
||||
"Registry: language code -> dictionary file path.")
|
||||
|
||||
(defvar ekp-hyphen--langs-short (make-hash-table :test 'equal)
|
||||
"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
|
||||
|
||||
(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)
|
||||
"Scan DIR for .dic files, populate language registry."
|
||||
(dolist (file (directory-files dir t "\\.dic\\'"))
|
||||
(let* ((name (file-name-nondirectory file))
|
||||
(lang (replace-regexp-in-string "\\(^hyph_\\|\\.dic$\\)" "" name))
|
||||
(normalized (ekp-hyphen--normalize-lang lang))
|
||||
(short (car (split-string lang "_"))))
|
||||
(puthash lang file ekp-hyphen--langs)
|
||||
(puthash normalized file ekp-hyphen--langs)
|
||||
(unless (gethash short ekp-hyphen--langs-short)
|
||||
(puthash short file ekp-hyphen--langs-short)))))
|
||||
|
||||
(defun ekp-hyphen--resolve-lang (lang)
|
||||
"Resolve LANG to dictionary path, trying exact then short forms."
|
||||
(or (gethash lang ekp-hyphen--langs)
|
||||
(let* ((norm (downcase (replace-regexp-in-string "-" "_" lang)))
|
||||
(let* ((norm (ekp-hyphen--normalize-lang lang))
|
||||
(parts (split-string norm "_"))
|
||||
found)
|
||||
(setq found (gethash norm ekp-hyphen--langs))
|
||||
(while (and parts (not found))
|
||||
(setq found (gethash (string-join parts "_")
|
||||
ekp-hyphen--langs-short)
|
||||
@ -96,10 +132,16 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
||||
(when (> end start)
|
||||
(list letters start (cl-subseq values start end))))))
|
||||
|
||||
(defun ekp-hyphen--compile (path)
|
||||
"Compile dictionary at PATH into ekp-hyphen struct."
|
||||
(defun ekp-hyphen--compile (path &optional language)
|
||||
"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))
|
||||
(maxlen 0))
|
||||
(maxlen 0) (left 2) (right 2) (alternative-count 0))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents path)
|
||||
(forward-line 1) ; skip encoding line
|
||||
@ -108,22 +150,36 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
||||
(point) (line-end-position))))
|
||||
(skip (or (string-empty-p line)
|
||||
(string-match-p "^[%#]\\|HYPHENMIN" line)
|
||||
(string-match-p "/" line)))) ; skip alt patterns
|
||||
(unless skip
|
||||
(string-match-p "/" line))))
|
||||
(cond
|
||||
((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
|
||||
(setq line (replace-regexp-in-string
|
||||
"\\^\\^\\([0-9a-fA-F]\\{2\\}\\)"
|
||||
(lambda (m) (string (string-to-number
|
||||
(match-string 1 m) 16)))
|
||||
line))
|
||||
(when-let ((parsed (ekp-hyphen--parse-pattern line)))
|
||||
(when-let* ((parsed (ekp-hyphen--parse-pattern line)))
|
||||
(puthash (car parsed) (cdr parsed) patterns)
|
||||
(setq maxlen (max maxlen (length (car parsed)))))))
|
||||
(setq maxlen (max maxlen (length (car parsed))))))))
|
||||
(forward-line 1)))
|
||||
(when (> alternative-count 0)
|
||||
(signal 'ekp-hyphen-unsupported-pattern
|
||||
(list (or language path) path alternative-count)))
|
||||
(ekp-hyphen--create :patterns patterns
|
||||
:cache (make-hash-table :test 'equal)
|
||||
:maxlen maxlen
|
||||
:left 2 :right 2)))
|
||||
:left left :right right)))
|
||||
|
||||
;;; Hyphenation Algorithm
|
||||
|
||||
@ -153,48 +209,65 @@ E.g., \"a1bc2\" -> letters=\"abc\", values=(0 1 0 2)."
|
||||
(nreverse result))))
|
||||
|
||||
(defun ekp-hyphen--positions (h word)
|
||||
"Get cached break positions for WORD."
|
||||
"Return cached break positions for WORD using hyphenator H."
|
||||
(let* ((key (downcase word))
|
||||
(cache (ekp-hyphen-cache h)))
|
||||
(or (gethash key cache)
|
||||
(puthash key (ekp-hyphen--compute h word) cache))))
|
||||
(cache (ekp-hyphen-cache h))
|
||||
(cached (gethash key cache ekp-hyphen--cache-miss)))
|
||||
(if (eq cached ekp-hyphen--cache-miss)
|
||||
(puthash key (ekp-hyphen--compute h word) cache)
|
||||
cached)))
|
||||
|
||||
;;; Public API
|
||||
|
||||
(defun ekp-hyphen-create (&optional lang file left right)
|
||||
"Create hyphenator for LANG or dictionary FILE.
|
||||
LEFT/RIGHT: min chars before/after breaks (default 2)."
|
||||
LEFT/RIGHT override the minimum characters kept before/after breaks;
|
||||
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)))
|
||||
(unless path (error "No dictionary for: %s" lang))
|
||||
(unless path
|
||||
(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)
|
||||
(puthash path (ekp-hyphen--compile path)
|
||||
ekp-hyphen--cache))))
|
||||
(condition-case err
|
||||
(puthash path (ekp-hyphen--compile path lang)
|
||||
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)
|
||||
(ekp-hyphen--create :patterns (ekp-hyphen-patterns h)
|
||||
:cache (ekp-hyphen-cache h)
|
||||
:maxlen (ekp-hyphen-maxlen h)
|
||||
:left (or left 2) :right (or right 2))
|
||||
:left (or left (ekp-hyphen-left h))
|
||||
:right (or right (ekp-hyphen-right h)))
|
||||
h))))
|
||||
|
||||
(defun ekp-hyphen-positions (h word)
|
||||
"Return valid break positions in WORD, respecting margins."
|
||||
"Return valid break positions in WORD for H, respecting margins."
|
||||
(let ((left (ekp-hyphen-left h))
|
||||
(right (- (length word) (ekp-hyphen-right h))))
|
||||
(cl-remove-if-not (lambda (p) (and (>= p left) (<= p right)))
|
||||
(ekp-hyphen--positions h word))))
|
||||
|
||||
(defun ekp-hyphen-inserted (h word &optional hyphen)
|
||||
"Return WORD with HYPHEN inserted at break points."
|
||||
(let ((hyphen (or hyphen "-")) (result word) (off 0))
|
||||
(dolist (pos (ekp-hyphen-positions h word))
|
||||
(setq result (concat (substring result 0 (+ pos off))
|
||||
hyphen
|
||||
(substring result (+ pos off)))
|
||||
off (+ off (length hyphen))))
|
||||
result))
|
||||
"Return WORD with HYPHEN inserted at H's break points."
|
||||
(let ((positions (ekp-hyphen-positions h word)))
|
||||
(if (null positions)
|
||||
word
|
||||
(let ((hyphen (or hyphen "-")) (start 0) parts)
|
||||
(dolist (pos positions)
|
||||
(push (substring word start pos) parts)
|
||||
(push hyphen parts)
|
||||
(setq start pos))
|
||||
(push (substring word start) parts)
|
||||
(apply #'concat (nreverse parts))))))
|
||||
|
||||
(defun ekp-hyphen-boxes (h word)
|
||||
"Split WORD into syllables at break points."
|
||||
"Split WORD into syllables at H's break points."
|
||||
(split-string (ekp-hyphen-inserted h word " ") " "))
|
||||
|
||||
(provide 'ekp-hyphen)
|
||||
|
||||
290
ekp-utils.el
290
ekp-utils.el
@ -1,7 +1,23 @@
|
||||
;;; ekp-utils.el --- Utility functions for EKP -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024
|
||||
;; Author: emacs-kp contributors
|
||||
;; Copyright (C) 2024-2026 Kinney Zhang
|
||||
|
||||
;; Author: Kinney Zhang <kinneyzhang666@gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
@ -41,6 +57,10 @@ information is available (batch mode, tty frames)."
|
||||
(let ((family (face-attribute 'default :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)
|
||||
"Return non-nil if FONT-FAMILY appears to be monospace.
|
||||
Returns nil (unknown) when font information is unavailable."
|
||||
@ -56,6 +76,7 @@ Returns nil (unknown) when font information is unavailable."
|
||||
(= (aref info 7) (aref info 11))))))))
|
||||
|
||||
(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
|
||||
(insert string)
|
||||
(goto-char (point-min))
|
||||
@ -68,6 +89,7 @@ Returns nil (unknown) when font information is unavailable."
|
||||
(buffer-substring (point) (1+ (point))))))
|
||||
|
||||
(defun ekp-get-cjk-letter (string)
|
||||
"Return the first wide CJK character in STRING, or nil if none."
|
||||
(with-temp-buffer
|
||||
(insert string)
|
||||
(goto-char (point-min))
|
||||
@ -81,7 +103,9 @@ Returns nil (unknown) when font information is unavailable."
|
||||
(buffer-substring (point) (1+ (point))))))
|
||||
|
||||
(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))
|
||||
(font-family (ekp-font-family letter)))
|
||||
;; return monospace font family
|
||||
@ -90,25 +114,34 @@ Returns nil (unknown) when font information is unavailable."
|
||||
;; no latin letter in string, use default
|
||||
(face-attribute 'default :family)))
|
||||
|
||||
(declare-function ekp--measured-width "ekp")
|
||||
|
||||
(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
|
||||
;; as word spacing pixel
|
||||
(if-let ((font-family (ekp-monospace-p string)))
|
||||
(string-pixel-width
|
||||
(if-let* ((font-family (ekp-monospace-p string)))
|
||||
(ekp--measured-width
|
||||
(propertize " " 'face `(:family ,font-family)))
|
||||
(let* ((letter (ekp-get-latin-letter string))
|
||||
(font-family (ekp-font-family letter)))
|
||||
(string-pixel-width
|
||||
(ekp--measured-width
|
||||
(propertize
|
||||
" " 'face `(:family ,font-family))))))
|
||||
|
||||
(defun ekp-latin-font (string)
|
||||
(if-let ((letter (ekp-get-latin-letter string)))
|
||||
"Return the font family used for STRING's Latin letters.
|
||||
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)
|
||||
(face-attribute 'default :family)))
|
||||
|
||||
(defun ekp-cjk-font (string)
|
||||
(if-let ((letter (ekp-get-cjk-letter string)))
|
||||
"Return the font family used for STRING's CJK characters.
|
||||
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 "牛")))
|
||||
|
||||
@ -119,7 +152,7 @@ Returns nil (unknown) when font information is unavailable."
|
||||
(propertize " " 'display `(space :width (,pixel)))))
|
||||
|
||||
(defun ekp-cjk-fw-punct-p (str)
|
||||
"Return non-nil if STR starts with a CJK full-width punctuation char.
|
||||
"Return non-nil if STR begins with a CJK full-width punctuation char.
|
||||
Full-width alphanumerics (ABC, 123) are NOT punctuation."
|
||||
(let ((char (seq-first str)))
|
||||
(and
|
||||
@ -140,16 +173,16 @@ When STR is held as cjk-char, this checks if it still needs attachment."
|
||||
(memq (get-char-code-property char 'general-category)
|
||||
'(Ps Pi))))
|
||||
|
||||
(defun ekp--flush-latin-word (word boxes)
|
||||
"Push latin WORD to BOXES if non-nil. Return updated boxes."
|
||||
(if word (cons word boxes) boxes))
|
||||
(defun ekp--flush-latin-word (parts boxes)
|
||||
"Join reversed Latin PARTS once and push the word to BOXES."
|
||||
(if parts (cons (apply #'concat (nreverse parts)) boxes) boxes))
|
||||
|
||||
(defun ekp--flush-cjk-char (char boxes)
|
||||
"Push CJK CHAR to BOXES if non-nil. Return updated boxes."
|
||||
(if char (cons char boxes) boxes))
|
||||
(defun ekp--flush-cjk-char (parts boxes)
|
||||
"Join reversed CJK PARTS once and push the character to BOXES."
|
||||
(if parts (cons (apply #'concat (nreverse parts)) boxes) boxes))
|
||||
|
||||
(defun ekp--flush-spaces (spaces boxes prev-state next-width)
|
||||
"Push SPACES to BOXES based on context.
|
||||
(defun ekp--flush-spaces (parts boxes prev-state next-width)
|
||||
"Join reversed space PARTS and push them to BOXES based on context.
|
||||
PREV-STATE: 1=latin, 2=CJK (previous content type).
|
||||
NEXT-WIDTH: width of next character (1=latin, 2=CJK).
|
||||
Rules:
|
||||
@ -157,8 +190,9 @@ Rules:
|
||||
- CJK involved (prev or next is CJK): preserve all spaces
|
||||
- Latin-Latin with single space: let glue handle it
|
||||
- Latin-Latin with multiple spaces: preserve all but last"
|
||||
(when (and spaces (not (string-empty-p spaces)))
|
||||
(let ((cjk-involved (or (= prev-state 2) (= next-width 2))))
|
||||
(when parts
|
||||
(let ((spaces (apply #'concat (nreverse parts)))
|
||||
(cjk-involved (or (= prev-state 2) (= next-width 2))))
|
||||
(cond
|
||||
;; Leading spaces (no previous boxes): preserve all
|
||||
((null boxes)
|
||||
@ -166,6 +200,13 @@ Rules:
|
||||
;; CJK involved: preserve all spaces
|
||||
(cjk-involved
|
||||
(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
|
||||
((> (length spaces) 1)
|
||||
(setq boxes (cons (substring spaces 0 -1) boxes)))
|
||||
@ -173,69 +214,52 @@ Rules:
|
||||
(t nil))))
|
||||
boxes)
|
||||
|
||||
(defun ekp--flush-trailing-spaces (spaces boxes)
|
||||
"Push all trailing SPACES to BOXES (for end of string)."
|
||||
(if (and spaces (not (string-empty-p spaces)))
|
||||
(cons spaces boxes)
|
||||
(defun ekp--flush-trailing-spaces (parts boxes)
|
||||
"Join reversed trailing space PARTS once and push them to BOXES."
|
||||
(if parts
|
||||
(cons (apply #'concat (nreverse parts)) boxes)
|
||||
boxes))
|
||||
|
||||
(defun ekp--zero-width-attaching-p (char)
|
||||
"Return non-nil if zero-width CHAR must attach to the preceding text.
|
||||
Combining marks (Mn/Mc/Me), ZWJ/ZWNJ, CGJ and variation selectors
|
||||
attach to the previous character; other zero-width characters (such
|
||||
as zero-width space U+200B) are treated as invisible break points."
|
||||
Combining marks (Mn/Mc/Me), ZWJ/ZWNJ, CGJ, variation selectors and
|
||||
the word joiner attach to the previous character; other zero-width
|
||||
characters (such as zero-width space U+200B) are treated as
|
||||
invisible break points."
|
||||
(or (memq (get-char-code-property char 'general-category) '(Mn Mc Me))
|
||||
(memq char '(#x200C #x200D #x034F))
|
||||
(memq char '(#x200C #x200D #x034F #x2060 #xFEFF))
|
||||
(and (>= char #xFE00) (<= char #xFE0F))))
|
||||
|
||||
(defun ekp--handle-latin-char (str state latin-word cjk-char boxes)
|
||||
"Handle a latin (width=1) character.
|
||||
Return (new-state new-latin-word new-cjk-char new-boxes)."
|
||||
"Handle a latin (width=1) character STR.
|
||||
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the
|
||||
accumulators. Return (new-state new-latin-word new-cjk-char new-boxes)."
|
||||
(if (= state 1)
|
||||
;; Already in latin mode: accumulate
|
||||
(list 1 (concat latin-word str) nil boxes)
|
||||
;; Was in CJK mode: flush CJK char, switch to latin
|
||||
;; 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)))))
|
||||
(list 1 (cons str latin-word) nil boxes)
|
||||
;; Was in CJK mode: flush held CJK char, switch to latin
|
||||
(list 1 (list str) nil (ekp--flush-cjk-char cjk-char boxes))))
|
||||
|
||||
(defun ekp--handle-cjk-char (str state latin-word cjk-char boxes)
|
||||
"Handle a CJK (width=2) character.
|
||||
Return (new-state new-latin-word new-cjk-char new-boxes)."
|
||||
"Handle a CJK (width=2) character STR.
|
||||
STATE is the current mode; LATIN-WORD, CJK-CHAR and BOXES are the
|
||||
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)
|
||||
;; Was in latin mode: flush latin word, push CJK directly
|
||||
(let ((new-boxes (ekp--flush-latin-word latin-word boxes)))
|
||||
(if (ekp-cjk-opening-punct-p str)
|
||||
;; 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)))))))
|
||||
;; Was in latin mode: flush latin word, hold current CJK char
|
||||
(list 2 nil (list str) (ekp--flush-latin-word latin-word boxes))
|
||||
;; Already in CJK mode: flush held char, hold current
|
||||
(list 2 nil (list str) (ekp--flush-cjk-char cjk-char boxes))))
|
||||
|
||||
(defun ekp-split-to-boxes (string)
|
||||
"Split STRING into typographic boxes.
|
||||
Latin words become single boxes; CJK chars are individual boxes.
|
||||
Whitespace runs are preserved as separate boxes; CJK punctuation
|
||||
attaches to its neighboring char per kinsoku rules."
|
||||
Latin words become single boxes; CJK chars — punctuation included —
|
||||
are individual boxes. Whitespace runs are preserved as separate
|
||||
boxes. Kinsoku is enforced later via per-gap break permissions, not
|
||||
by merging boxes."
|
||||
(if (string-blank-p string)
|
||||
(vector string)
|
||||
(with-temp-buffer
|
||||
@ -243,9 +267,9 @@ attaches to its neighboring char per kinsoku rules."
|
||||
(goto-char (point-min))
|
||||
(let ((state (char-width (seq-first string))) ; 1=latin, 2=CJK
|
||||
(prev-state 1) ; track previous content state for space handling
|
||||
latin-word ; accumulator for latin characters
|
||||
cjk-char ; holds previous CJK char (for punct attachment)
|
||||
spaces ; accumulator for whitespace runs
|
||||
latin-word ; reversed fragments for latin characters
|
||||
cjk-char ; reversed fragments for one CJK character
|
||||
spaces ; reversed fragments for a whitespace run
|
||||
boxes) ; result list (built in reverse)
|
||||
(while (not (eobp))
|
||||
(let* ((str (buffer-substring (point) (1+ (point))))
|
||||
@ -256,24 +280,24 @@ attaches to its neighboring char per kinsoku rules."
|
||||
((and (= 0 width) (not (string-blank-p str))
|
||||
(ekp--zero-width-attaching-p char))
|
||||
(cond
|
||||
(latin-word (setq latin-word (concat latin-word str)))
|
||||
(cjk-char (setq cjk-char (concat cjk-char str)))
|
||||
(spaces (setq spaces (concat spaces str)))
|
||||
(boxes (setcar boxes (concat (car boxes) str)))
|
||||
(latin-word (push str latin-word))
|
||||
(cjk-char (push str cjk-char))
|
||||
(spaces (push str spaces))
|
||||
(boxes
|
||||
(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
|
||||
(t (setq latin-word str state 1))))
|
||||
(t (setq latin-word (list str) state 1))))
|
||||
;; Whitespace or other zero-width: flush content, accumulate spaces
|
||||
((or (string-blank-p str) (= 0 width))
|
||||
;; Don't flush opening punct - keep it held for attachment to next char
|
||||
(if (and cjk-char (ekp-cjk-opening-punct-p cjk-char))
|
||||
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 cjk-char nil)
|
||||
(setq boxes (ekp--flush-latin-word latin-word boxes))
|
||||
(when latin-word (setq prev-state 1))
|
||||
(setq latin-word nil)
|
||||
(setq spaces (concat spaces str)))
|
||||
(push str spaces))
|
||||
;; Non-whitespace: flush spaces first, then handle char
|
||||
(t
|
||||
(setq boxes (ekp--flush-spaces spaces boxes prev-state width))
|
||||
@ -300,29 +324,6 @@ attaches to its neighboring char per kinsoku rules."
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(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)
|
||||
"Load MODULE from a temp copy to allow rebuilding."
|
||||
(let ((tmpfile (make-temp-file
|
||||
@ -337,17 +338,13 @@ after CALLBACK returns."
|
||||
(declare-function ekp-c-init "ext:ekp")
|
||||
(declare-function ekp-c-version "ext:ekp")
|
||||
(declare-function ekp-c-thread-count "ext:ekp")
|
||||
(declare-function ekp-c-load-hyphenator "ext:ekp")
|
||||
|
||||
(defvar ekp-c-module-loaded nil
|
||||
"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 ()
|
||||
"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)))
|
||||
|
||||
(defun ekp-c-module-file ()
|
||||
@ -361,9 +358,28 @@ after CALLBACK returns."
|
||||
(defalias 'ekp-c-module-reload #'ekp--module-reload
|
||||
"Load MODULE from a temp copy to allow rebuilding.")
|
||||
|
||||
(defconst ekp-c-module-required-version "1.1"
|
||||
(defconst ekp-c-module-required-version "1.6"
|
||||
"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 ()
|
||||
"Load EKP C module if available.
|
||||
Refuses to enable a module older than
|
||||
@ -386,36 +402,36 @@ Run 'make' in ekp_c/ to rebuild; falling back to Elisp."
|
||||
(ekp-c-version) (ekp-c-thread-count)))))
|
||||
(message "C module not found. Run 'make' in ekp_c/ directory."))))
|
||||
|
||||
(defun ekp-c-load-dictionary (lang)
|
||||
"Load hyphenation dictionary for LANG into C module."
|
||||
(when ekp-c-module-loaded
|
||||
(let* ((root-dir (ekp-root-dir))
|
||||
(dict-file (expand-file-name
|
||||
(format "dictionaries/hyph_%s.dic" lang)
|
||||
root-dir)))
|
||||
(when (file-exists-p dict-file)
|
||||
(setq ekp-c-hyphenator-index
|
||||
(ekp-c-load-hyphenator dict-file))
|
||||
(when ekp-c-hyphenator-index
|
||||
(message "Loaded hyphenator for %s (index %d)"
|
||||
lang ekp-c-hyphenator-index))))))
|
||||
|
||||
(defun ekp-c-module-build ()
|
||||
"Build the C module using make."
|
||||
(interactive)
|
||||
(let ((module-dir (ekp-c-module-dir)))
|
||||
(if (and module-dir (file-exists-p
|
||||
;;;###autoload
|
||||
(defun ekp-c-module-build (&optional profile)
|
||||
"Build the C module with make using PROFILE.
|
||||
PROFILE is one of `portable', `native', `debug', or `sanitize';
|
||||
the default is `portable'."
|
||||
(interactive
|
||||
(list
|
||||
(intern
|
||||
(completing-read "C build profile: "
|
||||
'("portable" "native" "debug" "sanitize")
|
||||
nil t nil nil "portable"))))
|
||||
(setq profile (or profile 'portable))
|
||||
(unless (memq profile '(portable native debug sanitize))
|
||||
(user-error "Unknown EKP C build profile: %S" profile))
|
||||
(let* ((module-dir (ekp-c-module-dir))
|
||||
(makefile (and module-dir
|
||||
(expand-file-name "Makefile" module-dir)))
|
||||
(ekp-start-process-with-callback
|
||||
"ekp-c-build"
|
||||
(cond
|
||||
((eq system-type 'windows-nt)
|
||||
`("cmd.exe" "/c" ,(format "cd %s && make" module-dir)))
|
||||
(t `(,shell-file-name "-c" ,(format "cd %s && make" module-dir))))
|
||||
(lambda (_proc _buffer)
|
||||
(ekp-c-module-load)
|
||||
(message "ekp C module build success!")))
|
||||
(error "Makefile not found in ekp_c/ directory"))))
|
||||
(make (executable-find "make")))
|
||||
(unless (and makefile (file-exists-p makefile))
|
||||
(user-error "Makefile not found in ekp_c/ directory"))
|
||||
(unless make
|
||||
(user-error "The make executable is not available"))
|
||||
(let* ((default-directory (file-name-as-directory module-dir))
|
||||
(buffer (generate-new-buffer "*ekp-c-build*")))
|
||||
(make-process
|
||||
:name (generate-new-buffer-name "ekp-c-build")
|
||||
:buffer buffer
|
||||
:command (list make (format "PROFILE=%s" profile))
|
||||
:noquery t
|
||||
:sentinel (apply-partially #'ekp--c-build-finished profile)))))
|
||||
|
||||
(provide 'ekp-utils)
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
# 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.
|
||||
# Uses pthread for multi-threading, optimizes for native CPU.
|
||||
|
||||
@ -14,24 +17,39 @@ endif
|
||||
# Default to gcc on Windows if cc is not found
|
||||
ifeq ($(UNAME), Windows)
|
||||
CC = gcc
|
||||
# Default Emacs root for this environment (can be overridden)
|
||||
EMACS_ROOT ?= C:/Users/26289/Apps/emacs-30.2/emacs-30.2
|
||||
EMACS ?= $(EMACS_ROOT)/bin/emacs.exe
|
||||
# Point EMACS_ROOT at your Emacs installation (the directory that
|
||||
# contains include/emacs-module.h), e.g.:
|
||||
# make EMACS_ROOT=C:/path/to/emacs-30.2
|
||||
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
|
||||
CC ?= cc
|
||||
EMACS ?= emacs
|
||||
endif
|
||||
|
||||
CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -O3 -fPIC
|
||||
CFLAGS += -march=native -flto
|
||||
CFLAGS += -D_POSIX_C_SOURCE=200809L
|
||||
PROFILE ?= portable
|
||||
COMMON_CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -fPIC
|
||||
COMMON_CFLAGS += -D_POSIX_C_SOURCE=200809L
|
||||
|
||||
# Debug build
|
||||
ifdef DEBUG
|
||||
CFLAGS := -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fPIC
|
||||
CFLAGS += -fsanitize=address,undefined
|
||||
ifeq ($(PROFILE),portable)
|
||||
PROFILE_CFLAGS := -O3
|
||||
else ifeq ($(PROFILE),native)
|
||||
PROFILE_CFLAGS := -O3 -march=native -flto
|
||||
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
|
||||
|
||||
CFLAGS := $(COMMON_CFLAGS) $(PROFILE_CFLAGS)
|
||||
|
||||
# Platform-specific settings
|
||||
ifeq ($(UNAME), Darwin)
|
||||
# macOS
|
||||
@ -58,7 +76,7 @@ else
|
||||
endif
|
||||
|
||||
# Source files
|
||||
SRCS := ekp.c ekp_kp.c ekp_hyphen.c ekp_paragraph.c ekp_thread_pool.c
|
||||
SRCS := ekp.c ekp_kp.c ekp_thread_pool.c
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
|
||||
# Output
|
||||
@ -106,6 +124,7 @@ info:
|
||||
@echo "MODULE: $(MODULE)"
|
||||
@echo "UNAME: $(UNAME)"
|
||||
@echo "EMACS: $(EMACS)"
|
||||
@echo "PROFILE: $(PROFILE)"
|
||||
ifdef EMACS_INCLUDE
|
||||
@echo "EMACS_INCLUDE: $(EMACS_INCLUDE)"
|
||||
endif
|
||||
|
||||
119
ekp_c/README.md
119
ekp_c/README.md
@ -1,12 +1,18 @@
|
||||
# EKP C Dynamic Module
|
||||
|
||||
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.1).
|
||||
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.6).
|
||||
|
||||
The division of labor: **Elisp owns all font-dependent data**
|
||||
(tokenization, pixel measurement, glue values, prefix sums); the C
|
||||
module runs only the O(n²) dynamic program. This keeps the two engines
|
||||
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
|
||||
|
||||
```
|
||||
@ -15,77 +21,102 @@ ekp_c/
|
||||
├── ekp.c # Emacs module entry point (emacs_module_init)
|
||||
├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy
|
||||
├── 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
|
||||
```
|
||||
|
||||
Parallelism model: the DP for one paragraph is sequential (each
|
||||
position depends on all earlier ones), so the thread pool parallelizes
|
||||
across **paragraphs** via `ekp-c-break-batch` — the correct granularity,
|
||||
with zero synchronization in the inner loop.
|
||||
with zero synchronization in the inner loop. The pool is created
|
||||
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
|
||||
|
||||
```bash
|
||||
cd ekp_c
|
||||
make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)
|
||||
make PROFILE=portable # default → ekp.dylib/.so/.dll
|
||||
```
|
||||
|
||||
Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.
|
||||
Requirements: C11 compiler, Emacs module headers, pthreads.
|
||||
Windows builds need MinGW-w64 (for pthreads) and
|
||||
`make EMACS_ROOT=<path to your Emacs installation>`.
|
||||
|
||||
```bash
|
||||
make DEBUG=1 # Debug build with sanitizers
|
||||
make PROFILE=native # local CPU + LTO; benchmark-only
|
||||
make PROFILE=debug # -O0 with debug symbols
|
||||
make PROFILE=sanitize # ASan/UBSan with frame pointers
|
||||
make clean
|
||||
make info
|
||||
make info # includes the selected profile and final flags
|
||||
```
|
||||
|
||||
`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)
|
||||
|
||||
```elisp
|
||||
(ekp-c-init) ; init global state + thread pool
|
||||
(ekp-c-version) ; => "1.1" — checked by ekp-c-module-load
|
||||
(ekp-c-thread-count) ; => 8
|
||||
(ekp-c-init) ; init global state
|
||||
(ekp-c-version) ; => "1.6" — checked by ekp-c-module-load
|
||||
(ekp-c-thread-count) ; worker count (created lazily on first batch)
|
||||
(ekp-c-cleanup)
|
||||
|
||||
;; Synced automatically by ekp.el before every call:
|
||||
(ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO
|
||||
&optional CONSEC-HYPHEN LAST-SHORT)
|
||||
&optional CONSEC-HYPHEN LAST-SHORT EXTRA-STRETCH
|
||||
EMERGENCY-STRETCH)
|
||||
|
||||
;; Single paragraph (11 args):
|
||||
;; Single paragraph (15 args):
|
||||
(ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX
|
||||
GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES
|
||||
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH
|
||||
LEAD-SPACES TRAIL-SPACES)
|
||||
LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS
|
||||
TAIL-PROTRUDES HYPHEN-PROTRUDE
|
||||
FIRST-LINE-WIDTH)
|
||||
;; => (BREAKS . TOTAL-COST)
|
||||
|
||||
;; Many paragraphs in parallel: vector of 11-element vectors
|
||||
;; Many paragraphs in parallel: vector of 15-element vectors
|
||||
(ekp-c-break-batch PARAGRAPHS) ; => vector of (BREAKS . COST)
|
||||
```
|
||||
|
||||
`LEAD-SPACES` / `TRAIL-SPACES` are the space-box run widths that the
|
||||
Elisp renderer strips from line edges; the DP excludes them from line
|
||||
metrics so both layers agree exactly (new in 1.1).
|
||||
metrics so both layers agree exactly (since 1.1). `FORBIDDEN-POS`
|
||||
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
|
||||
Knuth-Plass pass, then — only when the paragraph end is unreachable —
|
||||
a second pass permitting emergency single-box breaks, so overlong
|
||||
unbreakable tokens can never make the result empty. Badness saturates
|
||||
at 10000 exactly like the Elisp side.
|
||||
a final pass that adds finite background emergency stretch to ordinary
|
||||
underfull candidates and still scores them through the same
|
||||
badness/fitness/demerits path. Separately, if an overfull candidate would
|
||||
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.
|
||||
|
||||
### Experimental: self-contained C path
|
||||
Failure behavior: the full schema is checked before extraction. Malformed
|
||||
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.
|
||||
|
||||
`ekp-c-break-lines` tokenizes and hyphenates in C
|
||||
(`ekp_paragraph.c`, `ekp_hyphen.c`) with a measurement callback into
|
||||
Emacs. ekp.el does **not** use this path; its tokenizer is a
|
||||
simplified approximation of `ekp-split-to-boxes`. Kept for
|
||||
experimentation.
|
||||
|
||||
```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)
|
||||
```
|
||||
Every public pixel/position integer must fit signed 32-bit range. The DP
|
||||
uses 64-bit intermediates for sums and differences, so valid extreme inputs
|
||||
cannot overflow when line width and protrusion are combined.
|
||||
Validation uses one `extract_integer` call per integer value and clears the
|
||||
temporary non-local exit only when the value is not an integer; it does not
|
||||
round-trip through Lisp predicates and comparisons.
|
||||
|
||||
## Performance
|
||||
|
||||
@ -94,13 +125,21 @@ byte-compiled Elisp around the C calls, min of 3 cold-cache runs):
|
||||
|
||||
| Case | Elisp engine (compiled) | C engine |
|
||||
|:----------------------------|------------------------:|---------:|
|
||||
| justify text-zh.txt w=200 | 96 ms | 57 ms |
|
||||
| justify mixed text w=300 | 53 ms | 23 ms |
|
||||
| range-justify zh 340–380 | 294 ms | 75 ms |
|
||||
| range-justify mix 280–320 | 480 ms | 34 ms |
|
||||
| DP only, text-zh w=400 | 15 ms | 1.3 ms |
|
||||
| justify text-zh.txt w=200 | 150 ms | 41 ms |
|
||||
| justify mixed text w=300 | 82 ms | 31 ms |
|
||||
| range-justify zh 340–380 | 529 ms | 106 ms |
|
||||
| range-justify mix 280–320 | 762 ms | 52 ms |
|
||||
| DP only, text-zh w=400 | 30 ms | 2.5 ms |
|
||||
|
||||
The pure-DP speedup is ~12× (1.3 ms vs 15 ms); end-to-end gains are
|
||||
smaller because tokenization, measurement and rendering stay in Elisp.
|
||||
The C engine matters most for `range-justify` (many widths per text)
|
||||
and multi-paragraph batches.
|
||||
The pure-DP speedup is ~12×; end-to-end gains are smaller because
|
||||
tokenization, measurement and rendering stay in Elisp. The C engine
|
||||
matters most for `range-justify` (many widths per text) and
|
||||
multi-paragraph batches. Absolute numbers vary with the machine and
|
||||
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.158–1.326 ms p99 for C and 1.429–1.438 ms for
|
||||
pure Elisp on the same machine. These figures are separate from the
|
||||
deliberately source-loaded, fully instrumented evaluator.
|
||||
|
||||
683
ekp_c/ekp.c
683
ekp_c/ekp.c
@ -1,6 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
* Naming follows Emacs module convention: module name = file name.
|
||||
* (require 'ekp-c) loads ekp.so, entry point in ekp.c
|
||||
@ -10,6 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "ekp_module.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
@ -17,25 +28,140 @@
|
||||
/* Required for Emacs modules */
|
||||
int plugin_is_GPL_compatible;
|
||||
|
||||
/* Cached Emacs environment for callbacks */
|
||||
static emacs_env *current_env = NULL;
|
||||
static emacs_value measure_func = NULL;
|
||||
|
||||
/*
|
||||
* Pixel measurement callback that calls back into Emacs
|
||||
*/
|
||||
static int32_t emacs_measure_string(const char *text, size_t len)
|
||||
/* Public array values are signed 32-bit pixels. The DP uses wider
|
||||
* intermediates; rejecting an out-of-range API value is safer than
|
||||
* silently changing it. */
|
||||
static inline int32_t clamp32(intmax_t v)
|
||||
{
|
||||
if (!current_env || !measure_func)
|
||||
return len * 7; /* fallback: ~7 pixels per char */
|
||||
if (v > INT32_MAX) return INT32_MAX;
|
||||
if (v < INT32_MIN) return INT32_MIN;
|
||||
return (int32_t)v;
|
||||
}
|
||||
|
||||
emacs_value str = current_env->make_string(current_env, text, len);
|
||||
emacs_value result = current_env->funcall(current_env, measure_func, 1, &str);
|
||||
static bool lisp_predicate(emacs_env *env, const char *name, emacs_value value)
|
||||
{
|
||||
emacs_value result = env->funcall(
|
||||
env, env->intern(env, name), 1, (emacs_value[]){value});
|
||||
return env->is_not_nil(env, result);
|
||||
}
|
||||
|
||||
if (current_env->non_local_exit_check(current_env) != emacs_funcall_exit_return)
|
||||
return len * 7;
|
||||
static bool i32_value_p(emacs_env *env, emacs_value value)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return (int32_t)current_env->extract_integer(current_env, result);
|
||||
static bool finite_number_p(emacs_env *env, emacs_value value)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -67,75 +193,55 @@ static emacs_value Fekp_c_cleanup(emacs_env *env, ptrdiff_t nargs,
|
||||
return env->intern(env, "t");
|
||||
}
|
||||
|
||||
/*
|
||||
* ekp-c-load-hyphenator: Load hyphenation dictionary
|
||||
*/
|
||||
static emacs_value Fekp_c_load_hyphenator(emacs_env *env, ptrdiff_t nargs,
|
||||
emacs_value *args, void *data)
|
||||
{
|
||||
(void)nargs; (void)data;
|
||||
|
||||
if (!ekp_global) {
|
||||
emacs_value signal = env->intern(env, "error");
|
||||
emacs_value msg = env->make_string(env, "ekp-c not initialized", 21);
|
||||
env->non_local_exit_signal(env, signal, msg);
|
||||
return env->intern(env, "nil");
|
||||
}
|
||||
|
||||
/* Get dictionary path */
|
||||
ptrdiff_t size = 0;
|
||||
env->copy_string_contents(env, args[0], NULL, &size);
|
||||
char *path = malloc(size);
|
||||
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
|
||||
*/
|
||||
typedef struct {
|
||||
int32_t line;
|
||||
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]) ||
|
||||
!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]));
|
||||
out->hyphen = clamp32(env->extract_integer(env, args[1]));
|
||||
out->fitness = clamp32(env->extract_integer(env, args[2]));
|
||||
out->last_ratio = lisp_predicate(env, "floatp", args[3])
|
||||
? env->extract_float(env, args[3])
|
||||
: (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 ||
|
||||
out->last_short < 0.0 || out->extra_stretch < 0 ||
|
||||
out->emergency_stretch < 0)
|
||||
return "EKP C ratios and stretch values are outside valid ranges";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
|
||||
emacs_value *args, void *data)
|
||||
{
|
||||
@ -144,140 +250,26 @@ static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs,
|
||||
if (!ekp_global || nargs < 4)
|
||||
return env->intern(env, "nil");
|
||||
|
||||
ekp_global->line_penalty = env->extract_integer(env, args[0]);
|
||||
ekp_global->hyphen_penalty = env->extract_integer(env, args[1]);
|
||||
ekp_global->fitness_penalty = env->extract_integer(env, args[2]);
|
||||
ekp_global->last_line_ratio = env->extract_float(env, args[3]);
|
||||
if (nargs > 4)
|
||||
ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]);
|
||||
if (nargs > 5)
|
||||
ekp_global->last_line_short_penalty = env->extract_float(env, args[5]);
|
||||
penalty_config_t config = {
|
||||
.consecutive = ekp_global->consec_hyphen_penalty,
|
||||
.last_short = ekp_global->last_line_short_penalty
|
||||
};
|
||||
const char *error = parse_penalties(env, nargs, args, &config);
|
||||
if (error)
|
||||
return signal_invalid_input(env, error);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
@ -300,17 +292,23 @@ static emacs_value Fekp_c_thread_count(emacs_env *env, ptrdiff_t nargs,
|
||||
emacs_value *args, void *data)
|
||||
{
|
||||
(void)nargs; (void)args; (void)data;
|
||||
return env->make_integer(env, EKP_THREAD_POOL_SIZE);
|
||||
/* Pool is created lazily; report its actual size once it exists,
|
||||
* 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
|
||||
*
|
||||
* Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks glue-stretches
|
||||
* hyphen-positions hyphen-width line-width)
|
||||
* Args: (ideal-prefix min-prefix max-prefix glue-ideals glue-shrinks
|
||||
* glue-stretches hyphen-positions hyphen-width line-width
|
||||
* lead-spaces trail-spaces forbidden-positions tail-protrudes
|
||||
* hyphen-protrude first-line-width)
|
||||
*
|
||||
* All 6 arrays must have consistent sizes:
|
||||
* - ideal/min/max-prefix: (n+1) elements
|
||||
* Array sizes must be consistent:
|
||||
* - ideal/min/max-prefix, lead/trail-spaces, tail-protrudes: n+1
|
||||
* - glue-ideals/shrinks/stretches: n elements
|
||||
*
|
||||
* Returns: (breaks . total-cost) where breaks is a list of box indices.
|
||||
@ -323,13 +321,15 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
||||
{
|
||||
(void)data;
|
||||
|
||||
if (!ekp_global || nargs < 11)
|
||||
if (!ekp_global || nargs < 15)
|
||||
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) */
|
||||
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 */
|
||||
|
||||
@ -354,18 +354,18 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
||||
|
||||
/* Extract prefix arrays */
|
||||
for (ptrdiff_t i = 0; i < prefix_len; i++) {
|
||||
ideal_prefix[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
|
||||
min_prefix[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
|
||||
max_prefix[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
|
||||
lead_spaces[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
|
||||
trail_spaces[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
|
||||
ideal_prefix[i] = clamp32(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)));
|
||||
max_prefix[i] = clamp32(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)));
|
||||
trail_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
|
||||
}
|
||||
|
||||
/* Extract glue arrays */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
glue_ideals[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
|
||||
glue_shrinks[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
|
||||
glue_stretches[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
|
||||
glue_ideals[i] = clamp32(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_stretches[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
|
||||
}
|
||||
|
||||
/* Get hyphen positions vector */
|
||||
@ -375,13 +375,51 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
||||
hyph_pos = malloc(hyph_count * sizeof(int32_t));
|
||||
if (hyph_pos) {
|
||||
for (ptrdiff_t i = 0; i < hyph_count; i++) {
|
||||
hyph_pos[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
|
||||
hyph_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t hyph_width = env->extract_integer(env, args[7]);
|
||||
int32_t line_width = env->extract_integer(env, args[8]);
|
||||
int32_t hyph_width = clamp32(env->extract_integer(env, args[7]));
|
||||
int32_t line_width = clamp32(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 */
|
||||
ekp_result_t *result = ekp_break_with_prefixes(
|
||||
@ -390,12 +428,16 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs,
|
||||
n,
|
||||
hyph_pos, hyph_count > 0 ? (size_t)hyph_count : 0,
|
||||
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(glue_ideals); free(glue_shrinks); free(glue_stretches);
|
||||
free(lead_spaces); free(trail_spaces);
|
||||
free(hyph_pos);
|
||||
free(forb_pos);
|
||||
free(tail_pro);
|
||||
|
||||
if (!result)
|
||||
return env->intern(env, "nil");
|
||||
@ -428,7 +470,10 @@ static bool extract_paragraph_data(
|
||||
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_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]);
|
||||
if (prefix_len <= 1)
|
||||
@ -455,17 +500,17 @@ static bool extract_paragraph_data(
|
||||
}
|
||||
|
||||
for (ptrdiff_t i = 0; i < prefix_len; i++) {
|
||||
(*ideal_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[0], i));
|
||||
(*min_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[1], i));
|
||||
(*max_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[2], i));
|
||||
(*lead_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[9], i));
|
||||
(*trail_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[10], i));
|
||||
(*ideal_prefix)[i] = clamp32(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)));
|
||||
(*max_prefix)[i] = clamp32(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)));
|
||||
(*trail_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i)));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < *n; i++) {
|
||||
(*glue_ideals)[i] = env->extract_integer(env, env->vec_get(env, args[3], i));
|
||||
(*glue_shrinks)[i] = env->extract_integer(env, env->vec_get(env, args[4], i));
|
||||
(*glue_stretches)[i] = env->extract_integer(env, env->vec_get(env, args[5], i));
|
||||
(*glue_ideals)[i] = clamp32(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_stretches)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i)));
|
||||
}
|
||||
|
||||
*hyph_count = env->vec_size(env, args[6]);
|
||||
@ -474,13 +519,48 @@ static bool extract_paragraph_data(
|
||||
*hyph_pos = malloc(*hyph_count * sizeof(int32_t));
|
||||
if (*hyph_pos) {
|
||||
for (ptrdiff_t i = 0; i < *hyph_count; i++) {
|
||||
(*hyph_pos)[i] = env->extract_integer(env, env->vec_get(env, args[6], i));
|
||||
(*hyph_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*hyph_width = env->extract_integer(env, args[7]);
|
||||
*line_width = env->extract_integer(env, args[8]);
|
||||
*hyph_width = clamp32(env->extract_integer(env, args[7]));
|
||||
*line_width = clamp32(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;
|
||||
}
|
||||
@ -489,9 +569,12 @@ static bool extract_paragraph_data(
|
||||
* ekp-c-break-batch: Process multiple paragraphs in parallel
|
||||
*
|
||||
* 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 9 elements (same as ekp-c-break-with-arrays args).
|
||||
* Each element is a vector of 15 elements (same as
|
||||
* ekp-c-break-with-arrays args).
|
||||
* Returns vector of (breaks . total-cost) for each paragraph.
|
||||
*
|
||||
* This is the high-performance API for processing multi-paragraph text.
|
||||
@ -504,10 +587,31 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
||||
if (!ekp_global || nargs < 1)
|
||||
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]);
|
||||
if (para_count <= 0)
|
||||
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 */
|
||||
ekp_batch_input_t *inputs = calloc(para_count, sizeof(ekp_batch_input_t));
|
||||
int32_t **all_ideal = calloc(para_count, sizeof(int32_t *));
|
||||
@ -519,13 +623,15 @@ 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_lead = 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 ||
|
||||
!all_glue_i || !all_glue_sh || !all_glue_st || !all_hyph ||
|
||||
!all_lead || !all_trail) {
|
||||
!all_lead || !all_trail || !all_forb || !all_pro) {
|
||||
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_lead); free(all_trail);
|
||||
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||
return env->intern(env, "nil");
|
||||
}
|
||||
|
||||
@ -533,31 +639,35 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
||||
for (ptrdiff_t p = 0; p < para_count; p++) {
|
||||
emacs_value para_vec = env->vec_get(env, args[0], p);
|
||||
|
||||
/* Extract 11 arguments from this paragraph's vector */
|
||||
emacs_value para_args[11];
|
||||
for (int i = 0; i < 11; i++) {
|
||||
/* Extract 15 arguments from this paragraph's vector */
|
||||
emacs_value para_args[15];
|
||||
for (int i = 0; i < 15; i++) {
|
||||
para_args[i] = env->vec_get(env, para_vec, i);
|
||||
}
|
||||
|
||||
size_t n;
|
||||
ptrdiff_t hyph_count;
|
||||
int32_t hyph_width, line_width;
|
||||
ptrdiff_t hyph_count, forb_count;
|
||||
int32_t hyph_width, line_width, hyphen_protrude, first_line_width;
|
||||
|
||||
if (!extract_paragraph_data(env, para_args,
|
||||
&all_ideal[p], &all_min[p], &all_max[p],
|
||||
&all_glue_i[p], &all_glue_sh[p], &all_glue_st[p],
|
||||
&all_hyph[p], &n, &hyph_count,
|
||||
&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 */
|
||||
for (ptrdiff_t j = 0; j < p; 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_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(all_glue_i); free(all_glue_sh); free(all_glue_st); free(all_hyph);
|
||||
free(all_lead); free(all_trail);
|
||||
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||
return env->intern(env, "nil");
|
||||
}
|
||||
|
||||
@ -574,6 +684,12 @@ static emacs_value Fekp_c_break_batch(emacs_env *env, ptrdiff_t nargs,
|
||||
inputs[p].line_width = line_width;
|
||||
inputs[p].lead_spaces = all_lead[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 */
|
||||
@ -584,10 +700,11 @@ 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_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_forb[p]); free(all_pro[p]);
|
||||
}
|
||||
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_lead); free(all_trail);
|
||||
free(all_lead); free(all_trail); free(all_forb); free(all_pro);
|
||||
|
||||
if (!results)
|
||||
return env->intern(env, "nil");
|
||||
@ -655,6 +772,12 @@ int emacs_module_init(struct emacs_runtime *runtime)
|
||||
if ((size_t)env->size < sizeof(*env))
|
||||
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 */
|
||||
defun(env, "ekp-c-init", 0, 0, Fekp_c_init,
|
||||
"Initialize EKP C module with thread pool.");
|
||||
@ -662,42 +785,21 @@ int emacs_module_init(struct emacs_runtime *runtime)
|
||||
defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup,
|
||||
"Cleanup EKP C module resources.");
|
||||
|
||||
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,
|
||||
defun(env, "ekp-c-set-penalties", 4, 8, Fekp_c_set_penalties,
|
||||
"Set Knuth-Plass algorithm penalties.\n\n\
|
||||
LINE-PENALTY: base penalty per line break (default 10)\n\
|
||||
HYPHEN-PENALTY: penalty for hyphenated breaks (default 50)\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\
|
||||
CONSEC-HYPHEN-PENALTY: multiplier for consecutive hyphen runs (default 100)\n\
|
||||
LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\n\n\
|
||||
LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\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 \
|
||||
&optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY)");
|
||||
&optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY EXTRA-STRETCH \
|
||||
EMERGENCY-STRETCH)");
|
||||
|
||||
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,
|
||||
defun(env, "ekp-c-break-with-arrays", 15, 15, Fekp_c_break_with_arrays,
|
||||
"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\
|
||||
MIN-PREFIX: vector of min width prefix sums (n+1 elements)\n\
|
||||
@ -709,11 +811,16 @@ HYPHEN-POS: vector of hyphenable box indices (sorted)\n\
|
||||
HYPHEN-WIDTH: pixel width of hyphen character\n\
|
||||
LINE-WIDTH: target line width in pixels\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\n\
|
||||
TRAIL-SPACES: vector (n+1) of space-box run widths ending at box k-1\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\
|
||||
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 \
|
||||
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES)");
|
||||
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS \
|
||||
TAIL-PROTRUDES HYPHEN-PROTRUDE FIRST-LINE-WIDTH)");
|
||||
|
||||
defun(env, "ekp-c-version", 0, 0, Fekp_c_version,
|
||||
"Return EKP C module version string.");
|
||||
@ -723,10 +830,8 @@ HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH LEAD-SPACES TRAIL-SPACES)");
|
||||
|
||||
defun(env, "ekp-c-break-batch", 1, 1, Fekp_c_break_batch,
|
||||
"Break multiple paragraphs in parallel.\n\n\
|
||||
PARAGRAPHS: vector of paragraph data, each element is a vector of 11 items:\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\
|
||||
PARAGRAPHS: vector of paragraph data, each element a vector of the\n\
|
||||
same 15 items `ekp-c-break-with-arrays' takes, in the same order.\n\n\
|
||||
Returns vector of (BREAKS . COST) for each paragraph.\n\
|
||||
This is the high-performance API for multi-paragraph processing.\n\n\
|
||||
(fn PARAGRAPHS)");
|
||||
|
||||
@ -1,307 +0,0 @@
|
||||
/*
|
||||
* ekp_hyphen.c - Liang hyphenation algorithm implementation
|
||||
*
|
||||
* Fast, thread-safe hyphenation with pattern caching.
|
||||
* Uses FNV-1a hash for O(1) pattern lookup.
|
||||
*/
|
||||
|
||||
#include "ekp_module.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
/* FNV-1a hash constants */
|
||||
#define FNV_OFFSET 14695981039346656037ULL
|
||||
#define FNV_PRIME 1099511628211ULL
|
||||
|
||||
static uint64_t fnv1a_hash(const char *data, size_t len)
|
||||
{
|
||||
uint64_t hash = FNV_OFFSET;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
hash ^= (uint8_t)data[i];
|
||||
hash *= FNV_PRIME;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a pattern like "hy3ph" into letters and values.
|
||||
* Returns true on success.
|
||||
*/
|
||||
static bool parse_pattern(const char *pat, ekp_pattern_t *out)
|
||||
{
|
||||
size_t pat_len = strlen(pat);
|
||||
if (pat_len == 0 || pat_len >= EKP_MAX_PATTERN_LEN * 2)
|
||||
return false;
|
||||
|
||||
size_t pos = 0;
|
||||
size_t letter_idx = 0;
|
||||
size_t value_idx = 0;
|
||||
|
||||
memset(out->values, 0, sizeof(out->values));
|
||||
memset(out->letters, 0, sizeof(out->letters));
|
||||
|
||||
while (pos < pat_len) {
|
||||
/* Read optional digit */
|
||||
uint8_t digit = 0;
|
||||
if (isdigit((unsigned char)pat[pos])) {
|
||||
digit = pat[pos] - '0';
|
||||
pos++;
|
||||
}
|
||||
out->values[value_idx++] = digit;
|
||||
|
||||
/* Read letter if present */
|
||||
if (pos < pat_len && !isdigit((unsigned char)pat[pos])) {
|
||||
out->letters[letter_idx++] = pat[pos];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
out->len = letter_idx;
|
||||
|
||||
/* Find non-zero range */
|
||||
size_t start = 0, end = value_idx;
|
||||
while (start < end && out->values[start] == 0) start++;
|
||||
while (end > start && out->values[end - 1] == 0) end--;
|
||||
|
||||
out->offset = start;
|
||||
|
||||
/* Shift values to start */
|
||||
if (start > 0) {
|
||||
memmove(out->values, out->values + start, end - start);
|
||||
memset(out->values + (end - start), 0, start);
|
||||
}
|
||||
|
||||
return letter_idx > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load patterns from .dic file
|
||||
*/
|
||||
ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path)
|
||||
{
|
||||
FILE *fp = fopen(dict_path, "r");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
ekp_hyphenator_t *h = calloc(1, sizeof(*h));
|
||||
if (!h) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_rwlock_init(&h->lock, NULL);
|
||||
h->left_min = 2;
|
||||
h->right_min = 2;
|
||||
|
||||
/* First pass: count patterns */
|
||||
char line[256];
|
||||
size_t count = 0;
|
||||
|
||||
fgets(line, sizeof(line), fp); /* skip encoding line */
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
size_t len = strlen(line);
|
||||
if (len > 0 && line[len - 1] == '\n')
|
||||
line[--len] = '\0';
|
||||
|
||||
/* Skip empty, comments, HYPHENMIN, patterns with / */
|
||||
if (len == 0 || line[0] == '%' || line[0] == '#')
|
||||
continue;
|
||||
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
|
||||
continue;
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
/* Allocate patterns */
|
||||
h->patterns = calloc(count, sizeof(ekp_pattern_t));
|
||||
h->hash_size = count * 2; /* load factor 0.5 */
|
||||
h->hash_table = calloc(h->hash_size, sizeof(uint32_t));
|
||||
|
||||
if (!h->patterns || !h->hash_table) {
|
||||
ekp_hyphen_destroy(h);
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Second pass: parse patterns */
|
||||
rewind(fp);
|
||||
fgets(line, sizeof(line), fp); /* skip encoding line */
|
||||
|
||||
size_t idx = 0;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
size_t len = strlen(line);
|
||||
if (len > 0 && line[len - 1] == '\n')
|
||||
line[--len] = '\0';
|
||||
|
||||
if (len == 0 || line[0] == '%' || line[0] == '#')
|
||||
continue;
|
||||
if (strstr(line, "HYPHENMIN") || strchr(line, '/'))
|
||||
continue;
|
||||
|
||||
/* Handle ^^XX hex escapes */
|
||||
char decoded[256];
|
||||
char *dst = decoded;
|
||||
const char *src = line;
|
||||
while (*src) {
|
||||
if (src[0] == '^' && src[1] == '^' &&
|
||||
isxdigit((unsigned char)src[2]) &&
|
||||
isxdigit((unsigned char)src[3])) {
|
||||
char hex[3] = {src[2], src[3], 0};
|
||||
*dst++ = (char)strtol(hex, NULL, 16);
|
||||
src += 4;
|
||||
} else {
|
||||
*dst++ = *src++;
|
||||
}
|
||||
}
|
||||
*dst = '\0';
|
||||
|
||||
if (parse_pattern(decoded, &h->patterns[idx])) {
|
||||
/* Insert into hash table */
|
||||
uint64_t hash = fnv1a_hash(h->patterns[idx].letters,
|
||||
h->patterns[idx].len);
|
||||
size_t slot = hash % h->hash_size;
|
||||
|
||||
while (h->hash_table[slot] != 0) {
|
||||
slot = (slot + 1) % h->hash_size;
|
||||
}
|
||||
h->hash_table[slot] = idx + 1; /* 1-indexed */
|
||||
|
||||
if (h->patterns[idx].len > h->max_pattern_len)
|
||||
h->max_pattern_len = h->patterns[idx].len;
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
h->pattern_count = idx;
|
||||
fclose(fp);
|
||||
return h;
|
||||
}
|
||||
|
||||
void ekp_hyphen_destroy(ekp_hyphenator_t *h)
|
||||
{
|
||||
if (!h) return;
|
||||
pthread_rwlock_destroy(&h->lock);
|
||||
free(h->patterns);
|
||||
free(h->hash_table);
|
||||
free(h);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find pattern by letters (hash table lookup)
|
||||
*/
|
||||
static ekp_pattern_t *find_pattern(ekp_hyphenator_t *h,
|
||||
const char *letters, size_t len)
|
||||
{
|
||||
if (len == 0 || len > h->max_pattern_len)
|
||||
return NULL;
|
||||
|
||||
uint64_t hash = fnv1a_hash(letters, len);
|
||||
size_t slot = hash % h->hash_size;
|
||||
|
||||
for (size_t i = 0; i < h->hash_size; i++) {
|
||||
uint32_t idx = h->hash_table[slot];
|
||||
if (idx == 0)
|
||||
return NULL;
|
||||
|
||||
ekp_pattern_t *p = &h->patterns[idx - 1];
|
||||
if (p->len == len && memcmp(p->letters, letters, len) == 0)
|
||||
return p;
|
||||
|
||||
slot = (slot + 1) % h->hash_size;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute hyphenation positions for a word
|
||||
* Thread-safe (read lock)
|
||||
*/
|
||||
int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len,
|
||||
int8_t *positions, size_t max_pos)
|
||||
{
|
||||
if (!h || !word || len == 0 || len > EKP_MAX_WORD_LEN - 2)
|
||||
return 0;
|
||||
|
||||
/* Check cache first */
|
||||
uint64_t word_hash = fnv1a_hash(word, len);
|
||||
size_t cache_slot = word_hash % EKP_CACHE_SIZE;
|
||||
|
||||
pthread_rwlock_rdlock(&h->lock);
|
||||
|
||||
if (h->cache[cache_slot].hash == word_hash &&
|
||||
strncmp(h->cache[cache_slot].word, word, len) == 0) {
|
||||
int count = h->cache[cache_slot].pos_count;
|
||||
if (count <= (int)max_pos) {
|
||||
memcpy(positions, h->cache[cache_slot].positions,
|
||||
count * sizeof(int8_t));
|
||||
}
|
||||
pthread_rwlock_unlock(&h->lock);
|
||||
return count;
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&h->lock);
|
||||
|
||||
/* Compute hyphenation */
|
||||
char padded[EKP_MAX_WORD_LEN + 2];
|
||||
padded[0] = '.';
|
||||
for (size_t i = 0; i < len; i++)
|
||||
padded[i + 1] = tolower((unsigned char)word[i]);
|
||||
padded[len + 1] = '.';
|
||||
size_t padded_len = len + 2;
|
||||
|
||||
uint8_t prio[EKP_MAX_WORD_LEN + 3];
|
||||
memset(prio, 0, sizeof(prio));
|
||||
|
||||
/* Apply matching patterns */
|
||||
pthread_rwlock_rdlock(&h->lock);
|
||||
|
||||
for (size_t i = 0; i < padded_len - 1; i++) {
|
||||
for (size_t j = i + 1; j <= padded_len && j <= i + h->max_pattern_len; j++) {
|
||||
ekp_pattern_t *pat = find_pattern(h, padded + i, j - i);
|
||||
if (pat) {
|
||||
size_t val_len = pat->len + 1 - pat->offset;
|
||||
for (size_t k = 0; k < val_len && k < sizeof(pat->values); k++) {
|
||||
size_t pos = i + pat->offset + k;
|
||||
if (pos < sizeof(prio) && pat->values[k] > prio[pos])
|
||||
prio[pos] = pat->values[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&h->lock);
|
||||
|
||||
/* Collect odd positions (subtract 1 for padding offset) */
|
||||
int8_t result[EKP_MAX_WORD_LEN];
|
||||
int count = 0;
|
||||
|
||||
for (size_t i = 1; i < padded_len && count < EKP_MAX_WORD_LEN; i++) {
|
||||
if (prio[i] & 1) { /* odd = break allowed */
|
||||
int pos = (int)i - 1; /* adjust for leading '.' */
|
||||
/* Apply margin constraints */
|
||||
if (pos >= h->left_min && pos <= (int)len - h->right_min) {
|
||||
result[count++] = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Update cache */
|
||||
pthread_rwlock_wrlock(&h->lock);
|
||||
|
||||
h->cache[cache_slot].hash = word_hash;
|
||||
strncpy(h->cache[cache_slot].word, word, len);
|
||||
h->cache[cache_slot].word[len] = '\0';
|
||||
memcpy(h->cache[cache_slot].positions, result, count * sizeof(int8_t));
|
||||
h->cache[cache_slot].pos_count = count;
|
||||
|
||||
pthread_rwlock_unlock(&h->lock);
|
||||
|
||||
/* Copy to output */
|
||||
int out_count = count < (int)max_pos ? count : (int)max_pos;
|
||||
memcpy(positions, result, out_count * sizeof(int8_t));
|
||||
return out_count;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user