diff --git a/packages/mcp/src/lib/comment-anchor.ts b/packages/mcp/src/lib/comment-anchor.ts index 5c94250b..4c16456d 100644 --- a/packages/mcp/src/lib/comment-anchor.ts +++ b/packages/mcp/src/lib/comment-anchor.ts @@ -22,10 +22,14 @@ * inline markdown (`**bold**`, `` `code` ``, `[t](u)`), the raw locator will not * match the document's plain text. Exactly like edit_page_text's json-edit * fallback, we first try the verbatim selection and, ONLY if it anchors nowhere - * in the whole document, retry with `stripInlineMarkdown` applied. This decision - * is made ONCE per public call in `resolveAnchorSelection`, so the four public - * entry points (can/count/get/apply) all agree on which locator matched — the - * suggestion-uniqueness gate depends on count and can/get never disagreeing. + * in the whole document, retry with `stripInlineMarkdown` applied. `canAnchorInDoc`, + * `getAnchoredText` and `applyAnchorInDoc` share this decision via + * `resolveAnchorSelection`. `countAnchorMatches` keeps its OWN parallel exact-wins + * implementation (it needs a raw match COUNT, not a single resolved locator), kept + * deliberately in sync with `resolveAnchorSelection`: raw match ⇒ use raw, else fall + * back to the stripped count. All four therefore agree on which locator matched — + * the suggestion-uniqueness gate depends on count and can/get never disagreeing, so + * these two exact-wins implementations MUST stay in sync if either is changed. */ import { stripInlineMarkdown } from "./text-normalize.js"; diff --git a/packages/mcp/src/lib/text-normalize.ts b/packages/mcp/src/lib/text-normalize.ts index cd545366..997cbff0 100644 --- a/packages/mcp/src/lib/text-normalize.ts +++ b/packages/mcp/src/lib/text-normalize.ts @@ -92,6 +92,29 @@ export function stripBalancedWrappers(s: string): string { * ORIGINAL string is returned so a locator can never normalize down to "" and * match everything. */ +export function stripInlineMarkdown(s: string): string { + if (typeof s !== "string" || s.length === 0) return s; + + // 1 + 2. Shared link/image and balanced-wrapper passes. + let out = stripWrappersAndLinks(s); + + // 3. Trim leading/trailing decoration: whitespace, leftover markdown markers, + // and emoji (Extended_Pictographic plus the VS16 / ZWJ joiners, plus the + // regional-indicator range U+1F1E6–U+1F1FF for flag emoji, which are NOT + // Extended_Pictographic). The `u` flag enables the Unicode property escape. + // Anchored runs only — interior text and sentence punctuation are untouched. + const DECORATION = + "[\\s*_~\\x60\\p{Extended_Pictographic}\\u{1F1E6}-\\u{1F1FF}\\u{FE0F}\\u{200D}]+"; + out = out + .replace(new RegExp("^" + DECORATION, "u"), "") + .replace(new RegExp(DECORATION + "$", "u"), ""); + + // 4. Never normalize a locator down to nothing. + if (out.length === 0) return s; + + return out; +} + /** * Build a bounded "closest text" hint for an anchor/find MISS, shared by * edit_page_text (json-edit) and create_comment (client) so both surface the @@ -125,26 +148,3 @@ export function closestBlockHint( points.length > 120 ? points.slice(0, 120).join("") + "…" : hitBlock; return ` Closest block text: "${snippet}".`; } - -export function stripInlineMarkdown(s: string): string { - if (typeof s !== "string" || s.length === 0) return s; - - // 1 + 2. Shared link/image and balanced-wrapper passes. - let out = stripWrappersAndLinks(s); - - // 3. Trim leading/trailing decoration: whitespace, leftover markdown markers, - // and emoji (Extended_Pictographic plus the VS16 / ZWJ joiners, plus the - // regional-indicator range U+1F1E6–U+1F1FF for flag emoji, which are NOT - // Extended_Pictographic). The `u` flag enables the Unicode property escape. - // Anchored runs only — interior text and sentence punctuation are untouched. - const DECORATION = - "[\\s*_~\\x60\\p{Extended_Pictographic}\\u{1F1E6}-\\u{1F1FF}\\u{FE0F}\\u{200D}]+"; - out = out - .replace(new RegExp("^" + DECORATION, "u"), "") - .replace(new RegExp(DECORATION + "$", "u"), ""); - - // 4. Never normalize a locator down to nothing. - if (out.length === 0) return s; - - return out; -}