Skip to content

#8176-An IDT alias for one of the positions (5', internal, 3') must be unique in the whole library (another case) #10049

Open
guliaisaeva wants to merge 5 commits into
masterfrom
fix/8176-unique-idt-alias-validation
Open

#8176-An IDT alias for one of the positions (5', internal, 3') must be unique in the whole library (another case) #10049
guliaisaeva wants to merge 5 commits into
masterfrom
fix/8176-unique-idt-alias-validation

Conversation

@guliaisaeva
Copy link
Copy Markdown
Collaborator

@guliaisaeva guliaisaeva commented May 14, 2026

How the feature works? / How did you fix the issue?

(Screenshots, videos, or GIFs, if applicable)

  • Added IDT alias uniqueness validation by position across the whole monomer library.
  • Extended validation to both monomers and RNA presets (monomerGroupTemplate).
  • Ensured invalid monomers/presets are skipped while valid items from the same update continue loading.
  • Added detailed error messages with monomer/preset name, alias details, and conflicting IDT position.
  • Added tests for duplicate IDT aliases, shorthand SDF aliases (ep3, ep5, i), preset validation, and monomer/preset collisions.

@AlexeyGirin
Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown
Contributor

Copilot AI commented May 14, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in commit 6d06801 by merging origin/master and resolving the conflict in packages/ketcher-core/src/application/editor/Editor.ts, keeping both the IDT alias validation and the template name length validation. Screenshot: N/A (no UI changes).

Comment on lines +535 to +546
if (!aliasCollisionExists) {
aliasCollisionExists = getExistingMonomerGroupTemplates().some(
(template) => {
idtAliasCollision = findIdtAliasCollision(
newMonomer.props?.idtAliases,
template.definition.idtAliases,
);

return Boolean(idtAliasCollision);
},
);
}
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.

getExistingMonomerGroupTemplates() rebuilds the mapped/filtered list of group templates on every iteration of the newMonomersLibraryChunk.forEach. The same call also appears at line 701 inside the preset loop. With N incoming items and T existing templates this is O(N×T) work that recomputes identical data.

Suggest hoisting the call out of the loops (compute once before the monomer forEach, and once before the preset forEach, or maintain an index that's incrementally updated as items are added).

Note also: this check looks up existing group templates, but the monomer loop runs before the preset loop — so a monomer in the same chunk will not collide against a preset added later in the same updateMonomersLibrary call. If that's intentional (presets always "win"), worth a comment; otherwise the order matters.

Comment on lines +187 to +196
const formatIdtAliasDetails = (idtAliases?: IKetIdtAliases) =>
IDT_ALIAS_POSITIONS.map((position) => {
const alias = getIdtAliasesByPosition(idtAliases)[position];

return alias
? `IDT ${IDT_ALIAS_POSITION_LABEL[position]} alias "${alias}"`
: null;
})
.filter((value): value is string => Boolean(value))
.join(', ');
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.

getIdtAliasesByPosition(idtAliases) is called once per position inside the .map, so the same object is recomputed four times per call. Lift it out:

Suggested change
const formatIdtAliasDetails = (idtAliases?: IKetIdtAliases) =>
IDT_ALIAS_POSITIONS.map((position) => {
const alias = getIdtAliasesByPosition(idtAliases)[position];
return alias
? `IDT ${IDT_ALIAS_POSITION_LABEL[position]} alias "${alias}"`
: null;
})
.filter((value): value is string => Boolean(value))
.join(', ');
const formatIdtAliasDetails = (idtAliases?: IKetIdtAliases) => {
const aliasesByPosition = getIdtAliasesByPosition(idtAliases);
return IDT_ALIAS_POSITIONS.map((position) => {
const alias = aliasesByPosition[position];
return alias
? `IDT ${IDT_ALIAS_POSITION_LABEL[position]} alias "${alias}"`
: null;
})
.filter((value): value is string => Boolean(value))
.join(', ');
};

This helper is called per failed monomer/preset and inside the collision-error path, so the redundant recomputes compound.

collision?: ReturnType<typeof findIdtAliasCollision>,
) =>
collision
? ` Bad position IDT alias "${collision.value}" for ${
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.

"Bad position IDT alias" reads awkwardly — "position" modifying "alias" suggests a format issue rather than a uniqueness violation, which is confusing since this message is only appended when a collision was already detected and reported in the prefix.

Consider rephrasing for clarity, e.g.:

Suggested change
? ` Bad position IDT alias "${collision.value}" for ${
? ` Conflicting IDT ${IDT_ALIAS_POSITION_LABEL[collision.position]} alias "${collision.value}".`

).toBeUndefined();
});

it('should validate shorthand IDT position aliases from converted SDF data', () => {
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.

Test name says "shorthand IDT position aliases" (plural) "from converted SDF data", but the fixtures only exercise ep3. The PR adds shorthand support for ep3, ep5, and i (see getIdtAliasesByPosition mapping endpoint3/endpoint5/internal to their shorthand forms), so the other two are untested.

Either rename to clarify the scope (e.g. "...for ep3 shorthand alias") or extend the fixtures to cover ep5 and i as well — the latter is preferable since the shorthand mapping is the value being added here.

Comment on lines +691 to +710
let idtAliasCollision = this._monomersLibrary
.map((monomer) =>
findIdtAliasCollision(
templateDefinition.idtAliases,
monomer.props?.idtAliases,
),
)
.find(Boolean);

if (!idtAliasCollision) {
idtAliasCollision = getExistingMonomerGroupTemplates()
.filter((template) => template.ref !== templateRef.$ref)
.map((template) =>
findIdtAliasCollision(
templateDefinition.idtAliases,
template.definition.idtAliases,
),
)
.find(Boolean);
}
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.

This block (and the analogous block at lines 514-546 for monomers) duplicates the two-stage collision search: against existing monomers, then against existing group templates. The two flows have diverged subtly already — e.g. the monomer loop uses .some() with mutation, while this one uses .map().find(Boolean) building an intermediate array of length M.

Consider extracting a single helper that takes idtAliases and (optionally) a ref to exclude from the template scan, returning the collision (or undefined). That removes the duplication, unifies the error-message structure, and lets you replace .map(...).find(Boolean) with a short-circuiting for...of to avoid the intermediate allocation.

…6-unique-idt-alias-validation

# Conflicts:
#	packages/ketcher-core/__tests__/application/editor/Editor.test.ts
#	packages/ketcher-core/src/application/editor/Editor.ts
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.

An IDT alias for one of the positions (5', internal, 3') must be unique in the whole library (another case)

3 participants