Skip to content

Fix a dropped morpheme ID when a zero-width rule wraps one ending at the same node - #500

Open
johnml1135 wants to merge 1 commit into
masterfrom
fix/signature-drops-id-under-identity-rule
Open

johnml1135 wants to merge 1 commit into
masterfrom
fix/signature-drops-id-under-identity-rule

Conversation

@johnml1135

@johnml1135 johnml1135 commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Quick summary

Copy-only rules now preserve a wrapped affix's morpheme ID when both rules mark the same trailing shape node.
The fix only makes the correct signature reachable (41/100 fresh loads); the rest trace to a separate, unfixed ordering defect (#506).
Only two files change: one reordered marking call and one new HermitCrab regression test.

Where to look

  • SynthesisAffixProcessAllomorphRuleSpec.ApplyRhs -- the reordered fallback marking that fixes the drop.
  • IdentityRuleSignatureMorphDropTests -- two deterministic controls plus the fenos regression, which retries up to 100 fresh language loads.

Deliberately not included

Validation

  • dotnet build Machine.sln -- Build succeeded, 0 Warning(s), 0 Error(s).
  • dotnet test tests/SIL.Machine.Morphology.HermitCrab.Tests/SIL.Machine.Morphology.HermitCrab.Tests.csproj --no-build --no-restore -- Passed: 108, Failed: 0, Skipped: 0.
  • dotnet csharpier check . -- Checked 720 files, no issues.
  • pwsh -NoProfile -File scripts/comment-hygiene.ps1 -BaseRef origin/master -- comment-hygiene: clean (lines added since origin/master (merge base 7109531e)).

Reading this a year from now

SynthesisAffixProcessAllomorphRuleSpec.ApplyRhs marks morphs on the output shape in two places: a fallback for rules with no new output morphs (truncation and zero-width rules, such as a copy-only CopyFromInput), and a loop that re-marks each input morph the rule wraps. When a zero-width rule's own affix and an inner wrapped rule's affix end at the same trailing shape node, whichever call runs second keeps that node's morpheme ID. The fallback used to run first, so the outer rule's ID could overwrite or precede the wrapped rule's. Moving the fallback after the loop makes the wrapped rule's ID stick and the outer rule's ID sort after it, matching the order the fixture in IdentityRuleSignatureMorphDropTests expects.

Decisions, and why

The fenos test retries up to 100 fresh loads and passes on the first correct signature. Measured: 41/100 correct with the fix, 0/100 with it reverted. Asserting every attempt would be red even with the fix; a single assertion would be flaky. Retry-until-first-success is the one assertion the data supports, and it goes red with the fix reverted.

An earlier comment blamed BidirList's unseeded skip-list levels. The source does not support that: same-range annotations tie-break on Annotation.ListID, a call-order counter. The claim was removed.

Deferred, and what would unblock it

Making every fresh parse deterministic needs the separate ordering defect tracked in #506 fixed first; this PR does not attempt that, since the true source of the per-run variation is not yet identified (it is not BidirList's level randomization, per the reading above).

Prior measurements (superseded)

An earlier revision of this PR body reported "five green repetitions; suite 575 passed, one skipped, zero failed" and a seed experiment claiming "12/12 repeatability versus three unseeded outputs," attributed to forcing a fixed seed on BidirList's Random. Those numbers were not reproduced here and the causal claim they rested on did not hold up against the source; the 41/100 and 0/100 counts above are the current, directly measured replacement.

🤖 Generated with Claude Code


This change is Reviewable

@codecov-commenter

codecov-commenter commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.57%. Comparing base (2599204) to head (157c031).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #500   +/-   ##
=======================================
  Coverage   74.57%   74.57%           
=======================================
  Files         453      453           
  Lines       38314    38314           
  Branches     5280     5280           
=======================================
+ Hits        28573    28574    +1     
  Misses       8576     8576           
+ Partials     1165     1164    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@johnml1135
johnml1135 force-pushed the fix/signature-drops-id-under-identity-rule branch from 96d6b7f to 5aa7b39 Compare September 25, 2026 22:54
@johnml1135
johnml1135 requested a review from jtmaxwell3 October 21, 2026 17:30
…the same node

In SynthesisAffixProcessAllomorphRuleSpec.ApplyRhs, a zero-width rule's
fallback "no new output morphs" marking claimed the shared final shape
node before the loop that re-marks the wrapped rule's own morph, so the
wrapped rule's morpheme ID could be overwritten or ordered ahead of the
outer rule's. Move the fallback marking after that loop so the wrapped
allomorph keeps its ID and the outer rule's ID sorts after it.

Adds IdentityRuleSignatureMorphDropTests with two deterministic controls
and one regression that retries fresh language loads. Measured over 100
fresh loads: with the fix, the correct signature is reached 41/100
times; with the fix reverted, 0/100 -- confirmed by reverting the
production change and rerunning. The remaining gap under the fix is a
separate, unfixed ordering defect (tracked in #506); this PR does not
make output deterministic for every ordering, only reachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnml1135
johnml1135 force-pushed the fix/signature-drops-id-under-identity-rule branch from 5aa7b39 to 157c031 Compare October 23, 2026 11:09
// Truncation and zero-width rules have no new morph. Mark the last node after the
// loop, so a wrapped allomorph ending there keeps it and this rule's ID sorts after.
string morphID = output.MorphologicalRuleApplicationCount.ToString();
output.MarkMorph(new List<ShapeNode>() { output.Shape.Last }, _allomorph, morphID);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: F1. The wrapped morph now loses its last segment. MarkMorph re-parents node annotations (AnnotationList.Add calls node.Remove(false)), so marking Shape.Last after the loop moves that node from the wrapped morph to this zero-width rule. For fen + IDENT, the root morph keeps f e and IDENT takes n; for fenos, QTOP keeps no nodes. Before this change, the zero-width morph was the one left empty, which fits a rule that adds no segments. Traced through the code, not run.

{
const string CorrectSig = "FEN+PTOQ+QTOP+IDENT|fenos;FEN+PTOQ+QTOP|fenos";
bool sawCorrect = false;
for (int trial = 0; trial < 100 && !sawCorrect; trial++)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: F2. This test passes if any one of 100 loads gives the right signature, so it stays green even if the fix works only 1 time in 100. AGENTS.md asks for deterministic fixtures. That is fine as a stopgap if this becomes an every-attempt assertion once #506 is fixed.


private static string ParseWithFreshMorpher(Language language, string word)
{
// maxDegreeOfParallelism: 1 matches ConformanceMorpherFactory.Create's default

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: F3. These comments point to code that does not exist in this repo: ConformanceMorpherFactory.Create and useMemoization here (the memo was removed in #522), and BatchCommand.BuildSignature at line 110. A grep over src and tests finds none of the three.

[Test]
public void Fenos_IdentityWrapsOutermost_IsFlakyPendingASeparateOrderingDefect()
{
// Measured: fix reaches the correct signature 41/100 fresh loads; reverted, 0/100. The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: F4. Measured: ... 41/100 ...; reverted, 0/100 records history and will go stale after the next change. .claude/skills/code-comments/SKILL.md rules out history in comments, and the PR body already has these numbers.

@claude

claude Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor
  1. Verdict: request changes.
  2. Most important: F1 at src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisAffixProcessAllomorphRuleSpec.cs:206. The reorder changes which morph loses the shared last node; it does not stop the loss. The wrapped morph now gives up its final segment (for example, the fen root keeps only fe). The next rule rebuilds morphs from each node's owning morph (line 151), so the shortened range would also reach the right-environment check in AllomorphEnvironment.cs:93 (unverified). A real fix probably needs a way to attach a zero-width morph without taking a node from the morph it wraps.
  3. Counts: Critical 0, Important 1 (F1), Low 3 (F2-F4).
  4. What I ran: dotnet test on the HermitCrab tests filtered to IdentityRuleSignatureMorphDropTests: 3 passed. The full HermitCrab test project: 153 passed, 0 failed. gh pr checks 500: both builds pass; the advisory comment-hygiene check passes, which is not the strict scan.
  5. Not verified: I did not run F1. This session could not write a scratch program, so I traced it through Word.MarkMorph and AnnotationList.Add. I did not re-measure the 41/100 rate on the current base, which now includes Remove HermitCrab's analysis-cascade memo #522.

Public API, target frameworks, packages, and sillsdev/machine.py parity: None verified.

Findings: F1 new, unverified (traced, not run); F2 new; F3 new; F4 new.

Reviewed at 157c031

johnml1135 added a commit that referenced this pull request Oct 24, 2026
Pin full parse multiplicity and application histories from combined Machine
#500 and #556 in three derivatives of the #480 truncation grammar. Update
fixture-derived coverage, interaction ledgers and their audit pins.

The original grammar is unchanged. The complete expectations intentionally
remain red on the conformance branch until both engine fixes are applied.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants