From 6a8c7e04f5aee96e24ee9d0ad8bbb780744dc80b Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Thu, 20 Aug 2026 07:34:03 +0800 Subject: [PATCH] fix: harden post-audit contracts and test gates --- .github/workflows/ci.yml | 5 +- .phrase/docs/CHANGE.md | 3 + .phrase/docs/ISSUES.md | 7 ++ .../change_post_audit_hardening_20260820.md | 53 ++++++++++++ .../issue_buffer_width_atomicity_20260820.md | 23 ++++++ .../issue_c_position_validation_20260820.md | 19 +++++ .../issue_hyphen_locale_20260820.md | 20 +++++ ...ssue_integration_no_projection_20260820.md | 20 +++++ ...issue_live_backward_projection_20260820.md | 22 +++++ ...ive_evaluator_source_freshness_20260820.md | 20 +++++ .../issue_showcase_test_freshness_20260820.md | 25 ++++++ .../plan_post_audit_hardening_20260820.md | 82 +++++++++++++++++++ .../task_post_audit_hardening_20260820.md | 56 +++++++++++++ ekp-buffer.el | 7 +- ekp-hyphen.el | 9 +- ekp_c/ekp.c | 21 +++++ tests/ekp-buffer-tests.el | 42 +++++++++- tests/ekp-c-tests.el | 33 ++++++++ tests/ekp-live-commit-evaluator.el | 12 +++ tests/ekp-showcase.el | 7 +- tests/ekp-tests.el | 13 ++- tests/load-project-source.el | 27 ++++++ tests/run-live-commit-evaluator.sh | 1 + tests/run-tests-isolated.sh | 12 +-- tests/run-tests-random-order.el | 2 +- tests/run-tests.sh | 11 +-- 26 files changed, 518 insertions(+), 34 deletions(-) create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/plan_post_audit_hardening_20260820.md create mode 100644 .phrase/phases/phase-kp-overhaul-20260726/task_post_audit_hardening_20260820.md create mode 100644 tests/load-project-source.el diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fbb9f1..e0b2127 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,8 +167,5 @@ jobs: shell: pwsh run: > emacs -Q --batch -L . -L tests - -l tests/ekp-tests.el - -l tests/ekp-buffer-tests.el - -l tests/ekp-gui-tests.el - -l tests/ekp-c-tests.el + --eval "(load-file \"tests/load-project-source.el\")" -f ert-run-tests-batch-and-exit diff --git a/.phrase/docs/CHANGE.md b/.phrase/docs/CHANGE.md index aace108..e5fee11 100644 --- a/.phrase/docs/CHANGE.md +++ b/.phrase/docs/CHANGE.md @@ -2,6 +2,9 @@ ## phase-kp-overhaul-20260726 +- 2026-08-20 lock post-audit hardening goals and tasks (`task037`–`task042`, + `task030` continuation; `issue022`–`issue028` opened): + `.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` diff --git a/.phrase/docs/ISSUES.md b/.phrase/docs/ISSUES.md index fcf2d9b..536a140 100644 --- a/.phrase/docs/ISSUES.md +++ b/.phrase/docs/ISSUES.md @@ -21,3 +21,10 @@ - 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) diff --git a/.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md new file mode 100644 index 0000000..c6397e0 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md @@ -0,0 +1,53 @@ +# Change: Post-Audit Hardening 2026-08-20 + +## 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. + +## Verification + +- Source-first normal ERT: 294/294. +- Source-first seeded random ERT: 294/294, including all nine GUI verifier + tests previously omitted by the name filter. +- Per-test isolated ERT: 294/294 process runs logged `0 unexpected`. +- C focused ERT: 15/15; C portable build warning-clean; source fuzz: + 300/300 with zero failures; checkdoc, release, dictionary, and shell gates + pass. +- Corrected narrowed source evaluator: parity, zero-work, GC, conflicts, and + width non-regression pass, but source candidate p99 remains above 16 ms; + the performance issue is intentionally not closed. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md new file mode 100644 index 0000000..54df5b4 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md @@ -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 + 294/294. +- **Resolved At:** Pending user-visible confirmation. +- **Resolved By:** Developer implementation and verification. +- **Commit:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md new file mode 100644 index 0000000..0e3c78b --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md @@ -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:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md new file mode 100644 index 0000000..55e4a2b --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md @@ -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:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md new file mode 100644 index 0000000..cd5f750 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md @@ -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:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md new file mode 100644 index 0000000..28be311 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md @@ -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:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md new file mode 100644 index 0000000..10a4ce8 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md @@ -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:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md new file mode 100644 index 0000000..fec9b28 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md @@ -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 294-test + inventories, and GUI contract checks pass. +- **Resolved At:** Pending user-visible confirmation. +- **Resolved By:** Developer implementation and verification. +- **Commit:** Pending hardening commit. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/plan_post_audit_hardening_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/plan_post_audit_hardening_20260820.md new file mode 100644 index 0000000..7fc256b --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/plan_post_audit_hardening_20260820.md @@ -0,0 +1,82 @@ +# 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. diff --git a/.phrase/phases/phase-kp-overhaul-20260726/task_post_audit_hardening_20260820.md b/.phrase/phases/phase-kp-overhaul-20260726/task_post_audit_hardening_20260820.md new file mode 100644 index 0000000..acb0733 --- /dev/null +++ b/.phrase/phases/phase-kp-overhaul-20260726/task_post_audit_hardening_20260820.md @@ -0,0 +1,56 @@ +# Tasks: Post-Audit Hardening 2026-08-20 + +- 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 294/294. + +- 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 294/294, and + source-first isolated ERT 294/294 process runs. + +- 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 + 294/294. + +- 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 294-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 [ ] 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. diff --git a/ekp-buffer.el b/ekp-buffer.el index 12b8247..981543e 100644 --- a/ekp-buffer.el +++ b/ekp-buffer.el @@ -905,6 +905,7 @@ EFFECTIVE-END includes the source boundary owned by the line." (and current-prefix-arg (prefix-numeric-value current-prefix-arg)))))) (setq pixel (or pixel (ekp-buffer--effective-width))) + (ekp--validate-width pixel) (let ((start (min beg end)) (finish (max beg end))) (setq beg start @@ -925,7 +926,9 @@ EFFECTIVE-END includes the source boundary owned by the line." (setq ekp-buffer--conflicts nil) (dolist (range (ekp-buffer--paragraph-ranges beg end)) (ekp-buffer--layout-paragraph (car range) (cdr range) pixel)) - (ekp-buffer--install-integrations) + (if (or ekp-auto-justify-mode ekp-buffer--spans) + (ekp-buffer--install-integrations) + (ekp-buffer--remove-integrations)) (goto-char point-before) (when mark-before (set-marker (mark-marker) (marker-position mark-before))) @@ -1939,7 +1942,7 @@ When START and FINISH are non-nil, copy only that source interval." (native (and end (ekp-buffer--native-row-start end)))) (and start end (if native - (/= native start) + (> native start) (and (< start end) (> (ekp--measured-width (ekp-buffer--logical-substring start end)) diff --git a/ekp-hyphen.el b/ekp-hyphen.el index 704ead7..b211acc 100644 --- a/ekp-hyphen.el +++ b/ekp-hyphen.el @@ -73,22 +73,29 @@ LEFT/RIGHT: minimum chars before first / after last break." ;;; 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) diff --git a/ekp_c/ekp.c b/ekp_c/ekp.c index bb48572..3975fa2 100644 --- a/ekp_c/ekp.c +++ b/ekp_c/ekp.c @@ -94,6 +94,22 @@ static ptrdiff_t paragraph_field_length(int field, ptrdiff_t prefix_len) } } +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}; @@ -134,6 +150,11 @@ static const char *validate_paragraph_values(emacs_env *env, emacs_value *args) 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; } diff --git a/tests/ekp-buffer-tests.el b/tests/ekp-buffer-tests.el index 9a76088..7f0b6e3 100644 --- a/tests/ekp-buffer-tests.el +++ b/tests/ekp-buffer-tests.el @@ -827,7 +827,7 @@ (1- (point)) 'ekp-buffer--display)))))) (ert-deftest ekp-buffer-test-live-backward-wrap-crossing-commits-once () - "Deleting into the previous native row publishes one atomic plan." + "Deleting into the previous native row keeps the live transaction local." (let ((text "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu") (calls 0) @@ -845,8 +845,8 @@ (setq calls (1+ calls)) (apply original arguments)))) (call-interactively #'delete-backward-char) - (should (= calls 1)) - (should-not ekp-buffer--live-edit) + (should (= calls 0)) + (should ekp-buffer--live-edit) (should (equal (substring-no-properties (buffer-string)) (concat text " xy")))))))) @@ -2421,6 +2421,42 @@ keeps the buffer justified, and leaves it unmodified." (should-error (call-interactively #'ekp-justify-region) :type 'buffer-read-only))) +(ert-deftest ekp-buffer-test-justify-invalid-width-preserves-projection () + "Invalid manual widths signal before clearing an existing projection." + (ekp-buffer-test--with-text "invalid width must preserve this projection" + (ekp-justify-region (point-min) (point-max) 20) + (let ((before (buffer-substring (point-min) (point-max))) + (spans ekp-buffer--spans)) + (dolist (width '(0 -1 1.5 "80")) + (should-error (ekp-justify-region (point-min) (point-max) width) + :type 'user-error) + (should (equal-including-properties + (buffer-substring (point-min) (point-max)) before)) + (should (eq ekp-buffer--spans spans)))))) + +(ert-deftest ekp-buffer-test-justify-empty-removes-unused-integrations () + "Manual justification does not retain hooks when no span was installed." + (ekp-buffer-test--with-text "" + (ekp-justify-region (point-min) (point-max) 20) + (should-not ekp-buffer--spans) + (should-not ekp-buffer--filter-installed) + (should-not (memq #'ekp-buffer--before-change + before-change-functions)) + (should-not (memq #'ekp-buffer--after-layout-change + after-change-functions)))) + +(ert-deftest ekp-buffer-test-justify-foreign-only-removes-unused-integrations () + "A foreign-only paragraph does not retain manual EKP integrations." + (ekp-buffer-test--with-text "foreign ownership keeps this paragraph natural" + (put-text-property (point-min) (point-max) 'display "foreign") + (ekp-justify-region (point-min) (point-max) 20) + (should-not ekp-buffer--spans) + (should-not ekp-buffer--filter-installed) + (should-not (memq #'ekp-buffer--before-change + before-change-functions)) + (should-not (memq #'ekp-buffer--after-layout-change + after-change-functions)))) + ;;;; Commands and mode integration (ert-deftest ekp-buffer-test-no-break-public-commands () diff --git a/tests/ekp-c-tests.el b/tests/ekp-c-tests.el index 97e4f54..4c1e965 100644 --- a/tests/ekp-c-tests.el +++ b/tests/ekp-c-tests.el @@ -30,6 +30,12 @@ [0] [0] [0] [] 0 10 [0 0] [0 0] [] [0 0] 0 10)) +(defun ekp-c-tests--position-args () + "Return valid direct C arguments with several position slots." + (list [0 10 20 30] [0 10 20 30] [0 10 20 30] + [0 0 0] [0 0 0] [0 0 0] [0 2] 0 15 + [0 0 0 0] [0 0 0 0] [1 3] [0 0 0 0] 0 15)) + (ert-deftest ekp-c-test-rejects-non-vector-schema-field () "Schema errors signal the module's explicit input condition." (skip-unless (ekp-c-tests--available)) @@ -62,6 +68,33 @@ (should-error (apply #'ekp-c-break-with-arrays args) :type 'ekp-c-invalid-input))) +(ert-deftest ekp-c-test-rejects-invalid-break-position-vectors () + "Position vectors must be in range, strictly sorted, and unique." + (skip-unless (ekp-c-tests--available)) + (dolist (case '((6 [-1]) (6 [3]) (6 [2 1]) (6 [1 1]) + (11 [0]) (11 [4]) (11 [2 1]) (11 [2 2]))) + (let ((args (ekp-c-tests--position-args))) + (setf (nth (car case) args) (cadr case)) + (should-error (apply #'ekp-c-break-with-arrays args) + :type 'ekp-c-invalid-input)))) + +(ert-deftest ekp-c-test-accepts-position-boundaries () + "Valid edge positions preserve the direct API result contract." + (skip-unless (ekp-c-tests--available)) + (let* ((args (ekp-c-tests--position-args)) + (result (apply #'ekp-c-break-with-arrays args))) + (should (consp result)) + (should (listp (car result))) + (should (numberp (cdr result))))) + +(ert-deftest ekp-c-test-batch-rejects-invalid-position-vectors () + "Batch preflight applies the same position contract to every paragraph." + (skip-unless (ekp-c-tests--available)) + (let ((args (ekp-c-tests--position-args))) + (setf (nth 11 args) [0]) + (should-error (ekp-c-break-batch (vector (vconcat args))) + :type 'ekp-c-invalid-input))) + (ert-deftest ekp-c-test-batch-rejects-short-paragraph-vector () "Batch preflight validates each paragraph before indexing 15 fields." (skip-unless (ekp-c-tests--available)) diff --git a/tests/ekp-live-commit-evaluator.el b/tests/ekp-live-commit-evaluator.el index a23c633..b1bbadf 100644 --- a/tests/ekp-live-commit-evaluator.el +++ b/tests/ekp-live-commit-evaluator.el @@ -30,6 +30,9 @@ (defconst ekp-live-commit-evaluator--maximum-regression 15.0) (defconst ekp-live-commit-evaluator--default-gc-threshold gc-cons-threshold) +(defconst ekp-live-commit-evaluator--source-files + '("ekp-utils.el" "ekp-hyphen.el" "ekp.el" "ekp-buffer.el") + "Production source files loaded explicitly for every measured root.") (defconst ekp-live-commit-evaluator--corpus (concat " extraordinary editing continues smoothly 中文拉丁混排" @@ -53,6 +56,14 @@ (defvar ekp-live-commit-evaluator--signature-ms 0.0) (defvar ekp-live-commit-evaluator--cache-ms 0.0) +(defun ekp-live-commit-evaluator--load-source-root () + "Load every production file from `EKP_LIVE_COMMIT_CODE_ROOT'." + (let ((root (getenv "EKP_LIVE_COMMIT_CODE_ROOT"))) + (unless root + (error "EKP_LIVE_COMMIT_CODE_ROOT is required for measurement")) + (dolist (file ekp-live-commit-evaluator--source-files) + (load-file (expand-file-name file root))))) + (defun ekp-live-commit-evaluator--env-number (name fallback) "Return numeric environment variable NAME, or FALLBACK." (if-let ((value (getenv name))) @@ -692,6 +703,7 @@ (if (equal (getenv "EKP_LIVE_COMMIT_MODE") "compare") (ekp-live-commit-evaluator--compare) + (ekp-live-commit-evaluator--load-source-root) (ekp-live-commit-evaluator--write-record (ekp-live-commit-evaluator--measure-round))) diff --git a/tests/ekp-showcase.el b/tests/ekp-showcase.el index 4acf1f4..27d19d4 100644 --- a/tests/ekp-showcase.el +++ b/tests/ekp-showcase.el @@ -47,10 +47,9 @@ (mixed (concat "中英混排是 Emacs 里最常见的场景: The quick brown fox " "jumps over the lazy dog, 而 internationalization " "这样的长词在窄栏会按 Liang 模式断词。自动行内代码 " - "(ekp-pixel-justify paragraph-text target-width 'justify nil)" - ;; (propertize - ;; "(ekp-pixel-justify paragraph-text target-width 'justify nil)" - ;; 'face 'ekp-showcase-inline-code) + (propertize + "(ekp-pixel-justify paragraph-text target-width 'justify nil)" + 'face 'ekp-showcase-inline-code) " 可以在合法空白边界附近换行,但不会插入" " discretionary hyphen。")) (atoms (concat "行内原子演示:手动硬原子 " diff --git a/tests/ekp-tests.el b/tests/ekp-tests.el index 83d80bf..046b59e 100644 --- a/tests/ekp-tests.el +++ b/tests/ekp-tests.el @@ -279,7 +279,16 @@ Used to verify no content is lost by justification." (ert-deftest ekp-test-hyphen-lang-fallback () "Short language codes resolve to a dictionary." (should (ekp-hyphen-create "en")) - (should-error (ekp-hyphen-create "zz_XX"))) + (dolist (locale '("zz_XX" "zz-XX")) + (should-error (ekp-hyphen-create locale) + :type 'ekp-hyphen-dictionary-not-found))) + +(ert-deftest ekp-test-hyphen-normalized-locale-prefers-exact-dictionary () + "Equivalent locale spellings resolve before the short-code fallback." + (dolist (locale '("de_CH" "de-CH" "de_ch" "DE-CH")) + (should (equal (file-name-nondirectory + (ekp-hyphen--resolve-lang locale)) + "hyph_de_CH.dic")))) (ert-deftest ekp-test-hyphen-alternative-languages-fail-closed () "Replacement-pattern dictionaries must not degrade to plain Liang breaks. @@ -302,6 +311,8 @@ DP represents break-specific replacement widths." ;; Esperanto also contains slash-prefixed patterns whose libhyphen ;; meaning is not representable as an ordinary Liang pattern. (should-error (ekp-hyphen-create "eo") + :type 'ekp-hyphen-unsupported-pattern) + (should-error (ekp-hyphen-create "hu-HU") :type 'ekp-hyphen-unsupported-pattern)) (ert-deftest ekp-test-hyphen-alternative-error-reaches-public-dispatch () diff --git a/tests/load-project-source.el b/tests/load-project-source.el new file mode 100644 index 0000000..9c71410 --- /dev/null +++ b/tests/load-project-source.el @@ -0,0 +1,27 @@ +;;; load-project-source.el --- Load EKP test subjects from source -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Test bootstrap that prevents ignored or stale byte-code files from deciding +;; which implementation the suite exercises. + +;;; Code: + +(let ((root (file-name-directory + (directory-file-name + (file-name-directory (or load-file-name buffer-file-name)))))) + (dolist (file '("ekp-utils.el" + "ekp-hyphen.el" + "ekp.el" + "ekp-buffer.el" + "tests/ekp-showcase.el" + "tests/ekp-gui-verify.el" + "tests/ekp-tests.el" + "tests/ekp-buffer-tests.el" + "tests/ekp-gui-tests.el" + "tests/ekp-c-tests.el" + "tests/run-tests-random-order.el")) + (load-file (expand-file-name file root)))) + +(provide 'load-project-source) +;;; load-project-source.el ends here diff --git a/tests/run-live-commit-evaluator.sh b/tests/run-live-commit-evaluator.sh index ab77360..9f23b78 100755 --- a/tests/run-live-commit-evaluator.sh +++ b/tests/run-live-commit-evaluator.sh @@ -42,6 +42,7 @@ run_round() EKP_LIVE_COMMIT_LABEL=$label \ EKP_LIVE_COMMIT_ROUND=$round \ EKP_LIVE_COMMIT_OUTPUT=$output \ + EKP_LIVE_COMMIT_CODE_ROOT=$code_root \ "$EMACS_BIN" -Q --batch -L "$code_root" -L "$ROOT/tests" \ -l "$ROOT/tests/ekp-live-commit-evaluator.el" } diff --git a/tests/run-tests-isolated.sh b/tests/run-tests-isolated.sh index 5c453aa..1e3868e 100755 --- a/tests/run-tests-isolated.sh +++ b/tests/run-tests-isolated.sh @@ -9,20 +9,14 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" TESTS=$( "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \ - -l "$ROOT/tests/ekp-tests.el" \ - -l "$ROOT/tests/ekp-buffer-tests.el" \ - -l "$ROOT/tests/ekp-gui-tests.el" \ - -l "$ROOT/tests/ekp-c-tests.el" \ - --eval '(dolist (test (ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t)) + -l "$ROOT/tests/load-project-source.el" \ + --eval '(dolist (test (ert-select-tests t t)) (princ (format "%s\n" (ert-test-name test))))' ) for test_name in $TESTS; do echo "isolated ERT: $test_name" "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \ - -l "$ROOT/tests/ekp-tests.el" \ - -l "$ROOT/tests/ekp-buffer-tests.el" \ - -l "$ROOT/tests/ekp-gui-tests.el" \ - -l "$ROOT/tests/ekp-c-tests.el" \ + -l "$ROOT/tests/load-project-source.el" \ --eval "(ert-run-tests-batch-and-exit '$test_name)" done diff --git a/tests/run-tests-random-order.el b/tests/run-tests-random-order.el index 43b7a58..343320e 100644 --- a/tests/run-tests-random-order.el +++ b/tests/run-tests-random-order.el @@ -26,7 +26,7 @@ (interactive) (let* ((seed (string-to-number (or (getenv "EKP_TEST_SEED") "20260728"))) (tests - (ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t)) + (ert-select-tests t t)) (names (mapcar #'ert-test-name tests)) (selector (cons 'member (ekp-tests--permuted-names names seed)))) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 25a2acc..0ff69a6 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -9,19 +9,12 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" case "$ORDER" in "") exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \ - -l "$ROOT/tests/ekp-tests.el" \ - -l "$ROOT/tests/ekp-buffer-tests.el" \ - -l "$ROOT/tests/ekp-gui-tests.el" \ - -l "$ROOT/tests/ekp-c-tests.el" \ + -l "$ROOT/tests/load-project-source.el" \ -f ert-run-tests-batch-and-exit ;; --random-order) exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \ - -l "$ROOT/tests/ekp-tests.el" \ - -l "$ROOT/tests/ekp-buffer-tests.el" \ - -l "$ROOT/tests/ekp-gui-tests.el" \ - -l "$ROOT/tests/ekp-c-tests.el" \ - -l "$ROOT/tests/run-tests-random-order.el" \ + -l "$ROOT/tests/load-project-source.el" \ -f ekp-tests-run-random-order ;; *)