Conversation
Renames the model's tune vocabulary so per-block extra data belongs to
plugins rather than to a dedicated Block Tune entity:
- BlockTune entity -> PluginDataNode, with key deletion when a value is set
to undefined and an isEmpty getter
- TuneIndex -> PluginDataIndex, serialized as {"k":"plugin",...,"plugin":...}
- TuneModifiedEvent -> PluginDataModifiedEvent, BlockTuneEvents ->
PluginDataEvents
- BlockTuneName/BlockTuneSerialized -> PluginDataName/PluginDataSerialized
- the serialized block key `tunes` -> `plugins`
Behavior changes beyond the rename:
- BlockNode.updatePluginData creates a missing entry instead of throwing. The
node is created empty and its listener attached before the first update,
because PluginDataNode dispatches synchronously.
- entries live in a null-prototype record, and an empty plugin name throws, so
a document-supplied name like __proto__ is stored as plain data
- empty entries are left out of serialization
- EditorDocument.modifyData applies a Modified change addressed by a
PluginDataIndex, so undo/redo and remote operations can replay plugin data
- the bubbled event carries the acting user from the context instead of a
hardcoded 'user'
BREAKING CHANGE: the serialized block key `tunes` is now `plugins`, and the
serialized index kind "tune" is now "plugin".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- re-export the renamed model types (PluginDataName, PluginDataSerialized, PluginDataIndex, PluginDataModifiedEvent, createPluginDataName) - declare EditorjsPluginDataMap, a third augmentable map alongside EditorjsPluginApiMap and ToolPluginOptionsMap, and resolve a plugin's data shape through PluginDataFor<Id>. An undeclared id falls back to a plain record rather than never, since per-block data is often stored by a plugin that has no reason to publish its shape. - add BlocksAPI.getPluginData / updatePluginData and a `plugins` map on insert Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- BlocksAPI.getPluginData / updatePluginData, backed by BlocksManager, with the acting user defaulting to the configured one - insert() accepts a `plugins` map, and convert() carries plugin data over to the converted block - fix insert() dropping the block's data: BlocksManager spread `...data` into the block init while BlockNode reads a nested `data` key, so inserting with data silently produced an empty block. `plugins` is passed as its own key. - fix BlocksAPI.insert not forwarding `focus`, `plugins` and `userId` - add UndoRedoManager integration tests over a real model: undoing a first write removes the entry, redo restores it, and undoing one key leaves the plugin's other keys untouched - fix the BlocksAPI integration spec wiring a second, empty model into the API, which made every direct model read in it see an empty document Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- map PluginDataModifiedEvent to a Modify operation carrying the new and previous values, instead of logging it as an unknown event type - add a PluginDataIndex branch to OperationsTransformer#applyTransformation, which dispatches on the against-operation's index kind and previously threw 'Unsupported index type'. Without it the first remote plugin data change threw in OTClient's pending-operation reduce, in transformStacks for every remote event, and in ot-server's conflicting-operation reduce. - widen ModifyOperationData's payload from Record<any, any> to unknown, so a scalar plugin data value is representable, and narrow the two payload length reads that relied on the old type - move the ot-server fixtures off the `tunes` key Two concurrent writes to the same key stay untransformed: server order decides and the last applied one wins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mutation testing (scoped to the changed files, as CI does) pointed at four real gaps: - BlocksManager.insert: nothing asserted that block data is nested under a `data` key rather than spread, nor that the plugins map is forwarded and omitted when absent - convertBlock: nothing covered carrying plugin data to the converted block - EditorDocument.modifyData: the PluginDataIndex branch had no model-level test - BlockNode: the initial-data path of plugin node creation was unasserted Fixing the last one surfaced a bug in BlockNode.serialized: it assigned entries into a plain object, so a plugin named `__proto__` set the object's prototype instead of becoming a key and vanished from the serialized block. The entry map is now built with Object.fromEntries. The previous test only passed because its expected literal was broken the same way — writing the key as a computed property, to satisfy the naming-convention lint rule, is what exposed it. Scores for the changed files: model 90.38 (threshold 75), core 86.72, collaboration-manager 52.08 (threshold 0, unchanged file-level scores). No surviving mutants remain on lines this branch touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
composeDataFromVersion2 dropped each v2 block's `tunes` silently. It now maps them onto the v3 `plugins` key: - names are kept verbatim, so a v3 plugin that adopts a v2 tune's name finds its data without a rename table - object data is copied key by key - a primitive, an array or null goes under the `value` key, since a plugin entry is a key/value map while v2 tune data is any JSON value - a block with no tunes, or an empty map, produces no `plugins` key - the entry map is built with Object.fromEntries, so a tune named `__proto__` survives instead of setting a prototype Adds the file's first spec, including the converter's existing data handling, and an integration test taking a v2 document through the converter into the model and reading it back with api.blocks.getPluginData. The spec stubs DOMParser, which the pre-existing inline-fragment extraction needs and this package's node test environment lacks. That extraction stays untested, so the file's mutation score is low (25%, threshold 0) — it simply had no spec at all before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- model.md, events.md, index-serialization.md: PluginDataNode, PluginDataIndex (`k: "plugin"` with `plugin`/`key` fields), PluginDataModifiedEvent, and Index.pluginData - plugins.md: a new "Per-block plugin data" section for plugin authors — the two API calls, the EditorjsPluginDataMap augmentation, and the behaviors that bite (key removal, per-key undo granularity, last-writer-wins on the same key, preserved data for unregistered plugins, move/convert/split, v2 carry over) — plus the two new api.blocks rows and the third type map - diagrams: model-tree-structure, events-catalog, architecture-overview - docs-updater agent: map the renamed model entity to docs/model.md Mentions of the Block Tune *tool kind* stay: they belong to remove-block-tunes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every task is done except the final one, which is the ready-to-archive signal: leaving it unchecked keeps the archive gate quiet until a maintainer comments /archive on the PR, per CONTRIBUTING. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit Tests
Mutation Tests
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
plugin-block-datachange from #189. Stacked on #189 — review that one first; this PR's diff is implementation only.Makes per-block data a working, plugin-owned store. Today
BlockNodehas atunesstore that nothing above the Model can reach:updateTuneDatacrashes for an entry the block wasn't created with,BlocksAPIcan't read or write it, undo is a silent no-op, andCollaborationManagerdrops the event as "Unknown event type".What's in it
tunesstore becomesplugins, keyed by pluginname. Writing to an absent entry creates it; setting a key toundefinedremoves it; empty entries are left out of serialization.TuneIndex→PluginDataIndex,Index.tune()→Index.pluginData(),TuneModifiedEvent→PluginDataModifiedEvent,BlockTuneName/BlockTuneSerialized→PluginDataName/PluginDataSerialized, and theBlockTunemodel entity →PluginDataNode.EditorDocument.modifyDataappliesModifiedchanges addressed by aPluginDataIndex, so the generic inversion inUndoRedoManagerworks for plugin data.PluginDataModifiedEventbecomes aModifyoperation, andOperationsTransformergains a plugin-data branch so such an operation can also be the one transformed against — without it the transformer throws'Unsupported index type'on the first remote plugin-data change, on every client and onot-server.ModifyOperationData's payload widens fromRecord<any, any>so scalar values are representable.BlocksAPI.getPluginData({ block, plugin })andupdatePluginData({ block, plugin, data, userId? });insert/insertManyaccept apluginsmap;moveandconvertpreserve plugin data andsplitleaves it on the original block. A new augmentableEditorjsPluginDataMaptypes both accessors, alongsideEditorjsPluginApiMapandToolPluginOptionsMap.composeDataFromVersion2carries each v2 block'stunesintoplugins, keys verbatim, so a v3 plugin adopting a v2 tune's name finds its data. Object data is copied key by key; a primitive or array is stored undervalue. The converter droppedtunessilently before.Breaking (pre-1.0)
The serialized block key
tunes→plugins, and the serialized indexk: "tune"→k: "plugin".ot-serveris not a passthrough — it keeps its ownEditorJSModeland applies every operation — so clients and server deploy together.Notes for review
BlockTunetool contract is untouched here. It never read Model tune data; removing it is the next PR in the stack.pluginskey is under discussion in docs(openspec): propose replacing Block Tunes with plugins #189. If it changes, the rename is mechanical and lands here.## Purpose, which the mechanical fold writes as a placeholder) can only happen after/archive.Verified:
yarn build,yarn lintandyarn testclean; surviving mutants killed in the plugin-data paths;openspec validate plugin-block-data --type changepasses.🤖 Generated with Claude Code