Skip to content

fix: position-based Replace Inactive to avoid chained-regex corruption - #3

Merged
dionmcm merged 2 commits into
mainfrom
fix/replace-inactive-chained-regex-issue-2
Jun 15, 2026
Merged

fix: position-based Replace Inactive to avoid chained-regex corruption#3
dionmcm merged 2 commits into
mainfrom
fix/replace-inactive-chained-regex-issue-2

Conversation

@dionmcm

@dionmcm dionmcm commented Jun 15, 2026

Copy link
Copy Markdown
Member

Closes #2

Problem

replaceInactiveConceptsInSelection applied replacements sequentially against an accumulating result string. When one inactive concept's replacement target SCTID matched another inactive concept's ID in the same selection, the second iteration's regex matched inside text just inserted by the first — corrupting the output.

Fix

Mirrors the existing replaceSelection approach: locate each concept (code + optional pipe-delimited term) against the original text via extractAllConceptIds, then apply replacements in reverse positional order with replaceSubrange. Each replacement is now immune to text inserted by other iterations.

  • New pure helper LookupViewModel.replacingInactiveConcepts(in:with:)
  • 4 regression tests, including the exact synthetic reproducer from the issue

Verification

  • xcodebuild build — succeeded
  • Full Codeagogo-Unit test plan — all pass
  • Confirmed the new collision test fails against the old chained-regex logic and passes with the fix

Note: the Windows port (codeagogo-win TrayAppContext.cs) is tracked separately per the issue and is not touched here.

🤖 Generated with Claude Code

dionmcm and others added 2 commits June 15, 2026 13:47
replaceInactiveConceptsInSelection applied replacements sequentially against
an accumulating result string. When one inactive concept's replacement target
SCTID matched another inactive concept's ID in the same selection, the second
iteration's regex matched inside text just inserted by the first, corrupting
the output (issue #2).

Mirror the existing replaceSelection approach: locate each concept (code plus
optional pipe-delimited term) against the original text via extractAllConceptIds,
then apply replacements in reverse positional order with replaceSubrange. This
makes each replacement immune to text inserted by other iterations.

Adds the new pure LookupViewModel.replacingInactiveConcepts(in:with:) helper and
4 regression tests, including the exact synthetic reproducer from issue #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ment count

Two issues found in multi-agent review of this PR:

1. (Critical) The collision regression test was order-dependent: the old
   chained-regex code only corrupted the one-directional case (A→B, B→C) in one
   of two Dictionary iteration orders, so the test could pass against the buggy
   code. Replaced it with a 2-cycle swap (A→B, B→A) that corrupts in BOTH orders
   — verified the swap fails the old implementation regardless of order.

2. (Important) The "Replaced N inactive concepts" message used
   replacementsByCode.count (intent), but substitution is driven by a different
   extractor (extractAllConceptIds, which has a size cap) than the one that
   found the concepts (eclBridge.extractConceptIds). A concept that couldn't be
   located was silently not replaced yet still counted. replacingInactiveConcepts
   now returns the set of codes actually replaced; the count reflects reality and
   a warning is logged for any concept that could not be located.

Also tightened the method docstring and added empty/no-match and
actual-replaced-reporting tests. Full unit suite: 553 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dionmcm
dionmcm merged commit 30d25b4 into main Jun 15, 2026
6 checks passed
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.

Replace Inactive: chained regex replacements can corrupt output when concept IDs collide

1 participant