Skip to content

LT-22710: Make phonological rule formula cells non-editable - #1082

Draft
johnml1135 wants to merge 6 commits into
mainfrom
fix/phon-rule-formula-readonly
Draft

LT-22710: Make phonological rule formula cells non-editable#1082
johnml1135 wants to merge 6 commits into
mainfrom
fix/phon-rule-formula-readonly

Conversation

@johnml1135

@johnml1135 johnml1135 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Typing into a phonological rule formula no longer changes anything. Until now a rule cell rendered the referenced phoneme's or natural class's own Name/Abbreviation as editable text, so an edit that reached the view renamed that object across the entire project — every other rule referencing it changed with it.

The question on opening a diff with four production lines and three hundred test lines is why the tests outweigh the fix. Because the invariant was previously held by a keystroke filter — PatternView.OnKeyPress, itself added for LT-21888 — that covers exactly one input path. This branch moves enforcement into the view constructor, where nothing can route around it, and pins each layer with a test that goes red when that layer alone is removed. The review worth your time is whether a read-only rootsite breaks an interaction users still need, not whether the markings are right.

Where to look

  • Deletion still has to work with the rootsite read-only — DeleteKey_StillRaisesRemoveItemsRequested_WhenRootsiteIsReadOnly pins it.
  • ReadOnlyView = true is a broad switch (selection, focus, IME registration, cut/paste). AllowDisplaySelection restores visible selection; three per-control tests pin the shipped wiring.
  • PatternView is shared with the Complex Concordance pattern builder, which still runs editable. Nothing here alters its behaviour.
  • All three rule kinds inherit the fix from RuleFormulaVcBase; regular, metathesis and affix-process each have their own test.
  • OnKeyPress and PatternEditingHelper.CanCut/CanPaste are deliberately kept — still load-bearing for the other consumer.

Deliberately not here

  • Live IME / drag-and-drop verification in a running FLEx. The corruption is proven by test through ReplaceWithTsString; the user-facing input path is not.
  • ComplexConcPatternVc has a sibling defect — a crash, not corruption. Separate branch.
  • ConstChartVc appears guarded at cell level. Assessed by reading only.

One side effect worth knowing. ReadOnlyView = true also forces AcceptsReturn to false (SimpleRootSite.cs:942). PatternView ships Tab-false / Return-true, so Tab is unaffected and Return changes only in where it is disposed of — OnKeyPress already swallowed it, and it now propagates to the host instead of being trapped. The rule formula is a DataTree slice, not a dialog with an AcceptButton, so this should be inert. Reasoned, not verified.

Verification. MorphologyEditorDllTests 21/21, LexTextControlsTests 353 passed / 3 skipped, ITextDllTests 207 passed / 1 skipped. Against the test-only commit the reproduction suite is 5 failed / 0 passed. Not stacked.


Reading this a year from now — start here

The investigation notes and architecture review that produced this fix were deliberately deleted from the branch rather than merged; their content is synthesised into these sections. They were working documents — a one-time analysis whose conclusions are now carried by the code and its tests.

The short version: a rule formula cell is not free text, and never was meant to be. That invariant lived in exactly one place — a WM_CHAR filter — and anything reaching the root box by another route (IME composition being the realistic case for vernacular keyboards) walked straight past it into a shared domain object's name field.

The layer cake

Rule formula editing, model outward:

Layer Type
Model PhSegmentRule/PhRegularRulePhSegRuleRHS cells → PhPhonContext trees
View constructor RuleFormulaVcBasePatternVcBaseFwBaseVc
Rootsite PatternView
Control RuleFormulaControl + RegRule/AffixRule/MetaRule subclasses
Insert UI InsertionControl — a hotlink strip, not a menu
Slice RuleFormulaSlice

A "cell" is StrucDesc/StrucChange/LeftContext/RightContext on a PhSegRuleRHS. kfragNC and kfragTerminalUnit render the referenced PhNaturalClass's and PhTerminalUnit's real multistring properties — which is why an edit renamed the object rather than corrupting the rule.

PatternVcBase has exactly two subclasses and PatternView exactly two consumers, so the audit surface here is closed, not open-ended.

Decisions, and why

Enforce in the view constructor, not with another input filter. The obvious cheap fix is to intercept whatever new path let the edit through. That is how the bug got here: OnKeyPress was already such a fix. Marking fragments ktptNotEditable denies the edit at the layer that owns the question, so a future input path inherits the guard instead of needing its own.

Both layers were kept because both are load-bearing, established by ablation rather than assumed. Removing just the kfragTerminalUnit marking while leaving ReadOnlyView = true reproduces the corruption. So the fragment markings — not ReadOnlyView — are what actually block ReplaceWithTsString. ReadOnlyView does separate work: it unregisters the keyboard/IME controller hook and disables cut/paste. Neither alone is sufficient.

AllowDisplaySelection follows the existing InterlinPrintView/InterlinTaggingChild pattern. A read-only rootsite suppresses selection display by default, but a pattern editor needs the user to see what an insert or delete will act on.

Paths not taken

Removing OnKeyPress as now-redundant. It is not. An audit found bracket-glyph and fake-tag spans in MetaRuleFormulaVc, AffixRuleFormulaVc and PatternVcBase for which it remains the only guard. Marking those is out of scope here.

Removing PatternEditingHelper.CanCut/CanPaste. They look dead once the rootsite is read-only, but PatternView is shared with ComplexConcControl, which as of this branch still runs with ReadOnlyView = false. Deleting them here would silently enable paste there.

Update: the Complex Concordance branch does set ReadOnlyView = true, so once both land these two overrides become provably dead — SimpleRootSite's ReadOnlyView setter is EditingHelper.Editable = !value (SimpleRootSite.cs:907-944), and base EditingHelper.CanCut/CanPaste (EditingHelper.cs:3672, 3789) already return false when m_fEditable is false, with no dependence on the override. CanCopy is unaffected either way. Removing them is deliberately left to a follow-up after both branches are in, rather than making this branch depend on one that is not yet open.

Fixing ComplexConcPatternVc in the same branch. Different feature area, and a different failure mode: it binds no real domain fields, so it cannot reproduce this rename. It throws instead.

Surprising findings

The corruption is real and reproducible, not theoretical. The original report described it as "sometimes the user can modify the underlying forms." A test now drives IVwSelection.ReplaceWithTsString on a phoneme terminal unit and the real PhPhoneme.Name comes back as "CORRUPTED".

ReadOnlyView does not gate the low-level edit API. Setting it alone leaves ReplaceWithTsString working. This was measured, not reasoned about, and it is the reason the fix has two layers.

Three of the five fragment markings were initially untested — they could be deleted with the suite green. Found by mutation testing after the first implementation and closed since; the current suite catches each one individually.

Evidence

Each claim in "Where to look" is pinned by a mutation, run rather than argued:

Mutation Result
Delete kfragTerminalUnit marking 4 tests red, incl. PhPhoneme.Name = "CORRUPTED"
Delete kfragNC marking mock-level and end-to-end natural-class tests red
Delete kfragFeature / kfragPlusVariable / kfragMinusVariable corresponding fragment tests red
Flip ReadOnlyView back to false 3 per-control wiring tests red, all three rule kinds
Remove AllowDisplaySelection its test red, alone

The EditabilityRecordingEnv spy was extended to record AddProp (it previously threw), which is what the feature and variable fragments call.

Deferred, and what would unblock it
  • Live IME verification. Needs a running FLEx with an active IME; not available in this environment. The mechanism is proven; the user path is inferred.
  • ComplexConcPatternVc — sibling defect in the other PatternVcBase subclass, confirmed by probe: no UpdateProp override, so any bypassing edit throws an unhandled NotImplementedException. Own branch.
  • ConstChartVc:297 binds a shared CmPossibility in a chooser-driven cell — same defect shape — but appears pre-guarded at the cell level by MakeCellsMethod.cs:495. Assessed by code reading only; never exercised.
  • The base-class question. Both PatternVcBase subclasses have now had a bug in this family. Whether "pattern views are never free text" should be enforced in PatternVcBase/PatternView themselves, so a third subclass cannot reintroduce it, is worth deciding once the Complex Concordance fix lands.

This change is Reviewable

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Comment hygiene (advisory)

No comment-style violations in the lines this branch adds since origin/main.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   10m 40s ⏱️ -1s
5 813 tests +14  5 732 ✅ +14  81 💤 ±0  0 ❌ ±0 
5 822 runs  +14  5 741 ✅ +14  81 💤 ±0  0 ❌ ±0 

Results for commit 4fe7c4c. ± Comparison against base commit c373152.

♻️ This comment has been updated with latest results.

@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 38.45%. Comparing base (c373152) to head (4fe7c4c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1082      +/-   ##
==========================================
+ Coverage   38.28%   38.45%   +0.17%     
==========================================
  Files        1499     1499              
  Lines      350230   350236       +6     
  Branches    40261    40261              
==========================================
+ Hits       134096   134695     +599     
+ Misses     186915   186342     -573     
+ Partials    29219    29199      -20     
Files with missing lines Coverage Δ
Src/LexText/LexTextControls/PatternView.cs 22.26% <100.00%> (+22.26%) ⬆️
Src/LexText/Morphology/RuleFormulaControl.cs 7.71% <100.00%> (+7.71%) ⬆️
Src/LexText/Morphology/RuleFormulaVcBase.cs 20.14% <100.00%> (+20.14%) ⬆️

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@johnml1135
johnml1135 marked this pull request as draft August 19, 2026 18:55
Adds the failing tests that demonstrate Docs/bugs/phon-rule-direct-editing.md:
- RuleFormulaVcBaseEditabilityTests: RuleFormulaVcBase.Display never sets
  ktptEditable=NotEditable before AddStringAltMember for the natural-class
  abbreviation (kfragNC) or the terminal-unit name (kfragTerminalUnit),
  across RegRuleFormulaVc, MetaRuleFormulaVc, and AffixRuleFormulaVc.
- RuleFormulaDirectEditReproTests: drives a real IVwRootBox/PatternView and
  calls IVwSelection.ReplaceWithTsString directly (bypassing
  PatternView.OnKeyPress entirely, the same low-level path IME composition
  or drag-and-drop would use) and shows it actually renames the live
  PhPhoneme.Name.

All 5 tests fail against current code, confirming the defect by direct
reproduction rather than code reading alone.
Enforces "a rule cell is not free text" structurally instead of patching
another input path:

- RuleFormulaVcBase.Display now sets ktptEditable=NotEditable before every
  AddStringAltMember/AddProp call that binds a fragment to a real domain
  object's field (natural class abbreviation/name, terminal unit name) or to
  a computed feature/variable line. This is shared by RegRuleFormulaVc,
  MetaRuleFormulaVc, and AffixRuleFormulaVc, so all three rule kinds are
  covered by one change.
- RuleFormulaControl now sets m_view.ReadOnlyView = true. This also
  unregisters the keyboard/IME controller hook for the view (see
  SimpleRootSite.ReadOnlyView), closing the IME-composition bypass, not just
  the WM_CHAR path PatternView.OnKeyPress already filtered.
- PatternView.AllowDisplaySelection now always returns true (the established
  pattern also used by InterlinPrintView/InterlinTaggingChild), so the
  now-read-only rootsite still shows a visible selection for chooser
  insert/delete to act on.
- PatternView.OnKeyPress is left in place as defence in depth.

Adds a test confirming Delete still raises RemoveItemsRequested with the
rootsite read-only, and disposes the test view to avoid a finalizer-thread
COM cleanup race. All 6 reproduction/fix tests pass; MorphologyEditorDllTests
(13), LexTextControlsTests (356), and ITextDllTests (208) show no
regressions.
Documents which layer owns the invariant, an experiment isolating that the
view-constructor ktptEditable marking (not ReadOnlyView) is what actually
blocks IVwSelection.ReplaceWithTsString, what was deliberately left
unremoved (PatternEditingHelper's CanCut/CanPaste, shared with
ComplexConcControl) and unfixed (ComplexConcPatternVc, unaudited
fake-tag/literal spans elsewhere in the VC family), and manual verification
still needed in a running FLEx.
- Add RuleFormulaControlWiringTests: constructs the real Reg/Meta/Affix rule
  formula controls and asserts ReadOnlyView on the shipped rootsite, so the
  production RuleFormulaControl.cs wiring is covered rather than only the
  test-only view built in RuleFormulaDirectEditReproTests.
- Extend EditabilityRecordingEnv to record AddProp calls (previously it threw
  NotImplementedException on the exact call kfragFeature/kfragPlusVariable/
  kfragMinusVariable make) and add one test per fragment.
- Add AllowDisplaySelection_IsTrue_WhenRootsiteIsReadOnly.
- Add the natural-class equivalent of the phoneme ReplaceWithTsString repro:
  builds a real PhSimpleContextNC special-cased to display only its
  abbreviation, selects it through the real rootbox, and confirms a direct
  ReplaceWithTsString cannot rename PhNaturalClass.Abbreviation -- the same
  end-to-end standard already applied to the phoneme path.

Each addition was confirmed by ablation: flipping RuleFormulaControl's
ReadOnlyView back to false, deleting the three feature/variable ktptEditable
lines, removing AllowDisplaySelection, and removing kfragNC's ktptEditable
line each turn exactly the new, targeted test(s) red and nothing else; ablating
kfragNC's marking also reproduces "CORRUPTED" in PhNaturalClass.Abbreviation
end-to-end, mirroring the phoneme case.

Also corrects the review doc's characterization of ComplexConcPatternVc: it
has no real domain-field bindings to corrupt, so a direct edit throws
NotImplementedException out of UpdateProp rather than renaming anything --
a crash risk, not a data-corruption risk. Notes the audit surface is closed
(PatternVcBase has exactly two subclasses, PatternView exactly two
consumers) and flags ConstChartVc's apparent cell-level guard as
unverified/SUSPECTED-safe rather than confirmed clean.
The bug analysis and architecture self-review were working documents for
this fix. Their conclusions are now carried by the code and its tests; the
reasoning, decisions and paths not taken live in the pull request body so
they inform review without merging into the tree.
State why a fragment is not editable rather than restating what the next
line renders, drop a pointer to a test class and one to another view
constructor's internals, and cut an over-long implementation comment to
the sentence that matters. Re-wrap lines past the width limit.
@johnml1135
johnml1135 force-pushed the fix/phon-rule-formula-readonly branch from b2832bf to 4fe7c4c Compare August 19, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants