Files
gitmost/packages/mcp/test/unit/footnote-corpus.mjs
a 07ebd8c63e fix(footnotes): address PR #232 review — fragment-safe canonicalization, plugin placement parity, dead-code removal (#228)
Must-fix:
- Move canonicalizeFootnotes OUT of parseProsemirrorContent. It now runs only
  on FULL writes (createPage, updatePageContent operation==='replace'), never on
  an append/prepend fragment (a fragment would lose definition-only footnotes or
  synthesize a bogus empty list). Add a server binding spec.
- Match the live plugin's list PLACEMENT: a single already-canonical
  footnotesList is left exactly where it sits (the plugin never repositions a
  sole correct list), so the first write no longer reorders content that follows
  the list. Applied to BOTH the editor-ext copy and the MCP mirror; pinned by a
  shared golden corpus case with content after the list.
- Fix MCP tool count 38 -> 39 (README x3, AGENTS.md) and the transformJs param
  help (add canonicalizeFootnotes/insertInlineFootnote).

Simplifications:
- Remove the dead duplicate re-id mechanism (deriveFootnoteId/suffix/occurrence)
  from the PURE canonicalizer in both copies — references are never renamed, so
  the derived ids were never requested; first-wins-drop is the real behaviour.
  This also makes the editor-ext footnote-util note about "no cross-package copy"
  true again.
- Remove the sentinel round-trip in insertInlineFootnote: a generalized
  insertNodesAfterAnchor core inserts the footnoteReference node directly.
- Drop the redundant per-definition deep clone in step 4 (shallow id-normalizing
  copy; out is already deep-cloned).

Docs / architecture:
- Correct the editor-ext copy's "It exists because…" header to its real
  consumers (server import, page.service create/update, client paste).
- Note markdownToProseMirror reuse for create/update comment in collaboration.ts.
- A: shared golden JSON corpus exercised by BOTH the editor-ext copy and the MCP
  mirror (footnote-corpus.ts / .mjs) so "the two copies behave identically" is
  checkable.
- C: split the MCP canonicalizer into a pure mirror + footnote-authoring.ts.
- B: import services persist via a different path, so left one-line consolidation
  comments at the call sites rather than folding (does not fall out cleanly).

Tests: insertFootnote wrapper guards + docmost_transform dryRun auto-canonicalize
(MCP mock), page.service create/update + append/prepend binding (server jest),
shared corpus incl. nested-container reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 20:23:16 +03:00

1165 lines
25 KiB
JavaScript

// MIRROR (data only) of
// packages/editor-ext/src/lib/footnote/footnote-corpus.ts — keep the two in
// sync. Shared golden corpus for the footnote canonicalizer (issue #228): each
// case is { name, input, expected } where `expected` is exactly what
// `canonicalizeFootnotes(input)` must return. Running BOTH the editor-ext copy
// and this MCP mirror against the same corpus makes "the two pure copies behave
// identically" a checkable property without coupling the packages.
export const FOOTNOTE_CORPUS = [
{
"name": "out-of-order defs ordered by first reference",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "b"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "c"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "c"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "C"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "b"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "B"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "D"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "b"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "c"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "b"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "B"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "D"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "c"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "C"
}
]
}
]
}
]
}
]
}
},
{
"name": "orphan definition dropped",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "orphan"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "O"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
}
]
}
]
}
},
{
"name": "no references removes the list",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "plain"
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "orphan"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "O"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "plain"
}
]
}
]
}
},
{
"name": "reuse: repeated references collapse to one definition",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "text",
"text": " a "
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "shared"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "text",
"text": " a "
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "shared"
}
]
}
]
}
]
}
]
}
},
{
"name": "duplicate definitions: first wins",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "first"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "second"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "third"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "d"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "d"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "first"
}
]
}
]
}
]
}
]
}
},
{
"name": "synthesizes an empty definition for a reference with none",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "missing"
}
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "missing"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "missing"
},
"content": [
{
"type": "paragraph"
}
]
}
]
}
]
}
},
{
"name": "merges multiple footnotesList nodes into one",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "a"
},
{
"type": "footnoteReference",
"attrs": {
"id": "x"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "y"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "x"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "X"
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "tail"
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "y"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Y"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "a"
},
{
"type": "footnoteReference",
"attrs": {
"id": "x"
}
},
{
"type": "footnoteReference",
"attrs": {
"id": "y"
}
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "tail"
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "x"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "X"
}
]
}
]
},
{
"type": "footnoteDefinition",
"attrs": {
"id": "y"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Y"
}
]
}
]
}
]
}
]
}
},
{
"name": "single canonical list before a trailing empty paragraph stays put",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
}
]
},
{
"type": "paragraph"
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
}
]
},
{
"type": "paragraph"
}
]
}
},
{
"name": "single canonical list with NON-EMPTY content after it is NOT moved (plugin parity)",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "epilogue text"
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "x"
},
{
"type": "footnoteReference",
"attrs": {
"id": "a"
}
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "a"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "A"
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "epilogue text"
}
]
}
]
}
},
{
"name": "reference inside a nested container (callout) is collected",
"input": {
"type": "doc",
"content": [
{
"type": "callout",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "see "
},
{
"type": "footnoteReference",
"attrs": {
"id": "n"
}
}
]
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "n"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "note"
}
]
}
]
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "callout",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "see "
},
{
"type": "footnoteReference",
"attrs": {
"id": "n"
}
}
]
}
]
},
{
"type": "footnotesList",
"content": [
{
"type": "footnoteDefinition",
"attrs": {
"id": "n"
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "note"
}
]
}
]
}
]
}
]
}
},
{
"name": "no footnotes at all is unchanged",
"input": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "just text"
}
]
}
]
},
"expected": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "just text"
}
]
}
]
}
}
];