This is a step-by-step checklist for a coding agent porting DELEGATOR from @deutschemodelunitednations/munify-resolution-editor@0.1.x to 0.2.x. The library introduces a ResolutionStore abstraction; the old resolution + onResolutionChange props are gone.
DELEGATOR is a Path A consumer (single-user save flow, no real-time co-editing). We do not need the /yjs subpath; we'll wrap content in a createNativeStore and pipe its onChange into the existing resolutionContentStore.
Beyond just keeping current with the library, this fixes a real bug: the comment in newResolution/+page.svelte (around line 124) says
The
ResolutionEditorstores a$stateproxy in$resolutionContentStore, and proxy mutations don't trigger store subscriptions.
That is why the draft autosave currently polls every second. With 0.2, onChange fires deterministically after every mutator, so we can replace the polling loop with subscription-driven autosave.
- Library docs:
../munify-resolution-editor/README.md,../munify-resolution-editor/MIGRATION.md,../munify-resolution-editor/CHANGELOG.md - Reference implementation (Path A native store inside a modal):
../chase/src/lib/components/CreateAmendmentModal.svelte
- Create branch
feat/resolution-editor-v0.2 - Read
../munify-resolution-editor/MIGRATION.mdend-to-end (focus on "Path A — staying native") - Skim
../munify-resolution-editor/CHANGELOG.mdfor the removed-props table
- Bump
@deutschemodelunitednations/munify-resolution-editorto^0.2.0(or whatever the latest stable0.2is) - Run
bun install - No new peer deps to add — DELEGATOR does not need
yjs,y-protocols, ory-websocket
This is the central bridge — most of the migration happens here.
Goal: replace direct resolution + onResolutionChange props with a ResolutionStore instance, while keeping resolutionContentStore working as the external sink (so callers in paperhub/[paperId]/+page.svelte etc. don't have to change).
-
Remove the
$derivedofresolutionfrom$resolutionContentStore. -
Import
createNativeStore,createEmptyResolution, andtype ResolutionStorefrom the library:import { ResolutionEditor, createNativeStore, createEmptyResolution, type ResolutionStore } from '@deutschemodelunitednations/munify-resolution-editor';
-
Replace the body so the wrapper owns a
ResolutionStore:<script lang="ts"> // imports unchanged except as above import { get } from 'svelte/store'; import { untrack } from 'svelte'; let { committeeName, editable = true, headerData /* snippets */ }: Props = $props(); const labels = getResolutionLabels(); // Seed store from whatever was already in the external store, or an empty resolution. const initial = untrack(() => get(resolutionContentStore)) ?? createEmptyResolution(committeeName); const store: ResolutionStore = createNativeStore(initial, { onChange: (snapshot) => { // Mirror to the legacy svelte store so the rest of the app keeps working. resolutionContentStore.set(snapshot); } }); // External resets ($resolutionContentStore = undefined / saved draft restore) are // rare but real. When the external store is replaced wholesale, push it into the // editor store via replaceResolution (which preserves clause ids where possible). $effect(() => { const external = $resolutionContentStore; if (!external) return; if (external === store.snapshot) return; // skip self-echo // Cheap identity guard — if structurally equal, also skip. if (JSON.stringify(external) === JSON.stringify(store.snapshot)) return; store.replaceResolution(external); }); $effect(() => () => store.destroy()); function handleCopySuccess() { toast.success(m.phraseCopied()); } function handleCopyError() { toast.error(m.copyFailed()); } </script> <ResolutionEditor {store} {editable} {headerData} {labels} preamblePhrases={germanPreamblePhrases} operativePhrases={germanOperativePhrases} onCopySuccess={handleCopySuccess} onCopyError={handleCopyError} {clauseToolbar} {clauseAnnotations} {previewHeader} {previewFooter} />
-
Drop the
committeeName,resolution,onResolutionChangeprops from the<ResolutionEditor>invocation — they no longer exist on the library component. -
Verify that the wrapper still re-creates the store when
paperIdchanges. The current pattern (parent uses{#key paperId}or remounts viaeditorKey++) keeps the wrapper instance fresh, socreateNativeStore(initial, ...)runs again with the new content. Confirm this is the case in:src/routes/(authenticated)/dashboard/[conferenceId]/paperhub/[paperId]/+page.sveltesrc/routes/(authenticated)/dashboard/[conferenceId]/paperhub/newResolution/+page.sveltesrc/routes/(authenticated)/dashboard/[conferenceId]/paperhub/view/[paperId]/+page.svelte
If any of them keep the same wrapper mounted across paper switches, add a
{#key paperData?.id}block.
The big win here: replace the 1-second autosave polling loop with subscription-driven autosave.
-
Delete the
setInterval(saveCurrentDraft, 1000)setup (lines around 170–184). -
Subscribe to the existing
resolutionContentStorewith debouncing:import { resolutionContentStore } from '$lib/components/Paper/Editor/editorStore'; // ... existing code ... let saveTimer: ReturnType<typeof setTimeout> | null = null; $effect(() => { const unsubscribe = resolutionContentStore.subscribe((content) => { if (!browser || !draftStore || showRecoveryModal) return; if (content === undefined) return; if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveCurrentDraft(); saveTimer = null; }, 500); }); return () => { unsubscribe(); if (saveTimer) clearTimeout(saveTimer); }; });
-
Update the comment at line ~124 — the
$stateproxy issue is resolved by the newonChangecallback. Replace it with something like:Autosave debounces on
resolutionContentStoreupdates. The library's native store mirrors snapshots into the external store viaonChange, so subscriptions fire reliably. -
Keep
saveCurrentDraft()itself unchanged. Thebeforeunloadsafety net stays.
- No code change required — this file only reads/writes
$resolutionContentStorefor save/load. The wrapper still keeps the external store in sync. - Verify the wrapper is keyed on
paperData.idso a navigation from one paper to another reseeds the store. If it isn't, wrap the editor mount in{#key paperData?.id}.
- Same as
[paperId]/+page.svelte— no code change unless the wrapper isn't keyed.
-
Add the new exports to the re-export barrel so other parts of the app can also create native stores if needed:
export { // existing exports... createNativeStore, createEmptyNativeStore, type ResolutionStore, type TextHandle, type TextLocation, type ClausePath, type SubclausesBlockPath, type NativeStoreOptions } from '@deutschemodelunitednations/munify-resolution-editor';
- Open the file and check for any references to label keys
startEditingordoneEditing. These were removed in0.2. - Delete those keys from any custom labels object DELEGATOR builds.
- If a typed object literal that previously included these keys is now flagged by
bun run check, remove the keys.
- No structural change required.
resolutionContentStorestays aswritable<Resolution | undefined>()and is still the single source of truth for "what's currently being edited" from the perspective of save/draft callers. - Optional cleanup: rename
addToPanel('resolutionContentStore', …)debug label if you want, but not required.
After file-level changes, scan the repo for stragglers:
-
rg "onResolutionChange" src/— should return zero hits. -
rg "ResolutionEditor.*resolution=" src/— zero hits in JSX/svelte attributes. -
rg "startEditing|doneEditing" src/— zero hits in any messages or label files. -
rg "munify-resolution-editor" src/ | rg -v "0.2"— sanity-check there isn't a pinned old version anywhere outsidepackage.json.
-
bun run check— Svelte-check passes (no type errors). -
bun run lint— Prettier + ESLint pass. -
bun run dev— boots cleanly, no runtime errors in the browser console. - Manual smoke tests — open the app and verify each editor flow:
- New resolution (
/dashboard/[conferenceId]/paperhub/newResolution): pick agenda item, type into preamble + operative clauses, refresh page, verify draft restore modal offers correct content. - Existing draft paper (
/dashboard/[conferenceId]/paperhub/[paperId]for aWORKING_PAPER): edit clauses, click Save, verify content persists. Add a sub-clause; indent/outdent it. - View existing paper version (
/dashboard/[conferenceId]/paperhub/view/[paperId]): preview renders correctly. - Reviewer mode: open a working paper as a reviewer, verify quote selection still works.
- Switch between papers without page reload: verify the editor reseeds with the new paper's content (this is where missing
{#key}blocks bite).
- New resolution (
- Validation error path: open a paper with invalid content (e.g. craft one in DB studio); verify the validation error fallback still renders, the editor doesn't mount, and the raw content is preserved.
If something goes wrong in production:
- Revert the
package.jsonbump to^0.1.1. - Revert
ResolutionEditorWrapper.svelteandnewResolution/+page.svelte. - The schema is unchanged; nothing in the database needs touching.
- Y.js / real-time co-editing — DELEGATOR doesn't need it. Consumers stay single-user. Don't import from
/yjs. - Removing the
resolutionContentStorewritable — keeping it preserves the current save/load flow. A future refactor can collapse it into the native store directly, but that's not required for this migration. - Changing how
editorContentStore(TipTap content) works — onlyresolutionContentStoreis affected.
- One commit for the dependency bump + wrapper rewrite (atomic so revert is easy).
- One commit for the autosave polling → subscription change.
- One commit for any i18n/label cleanup.
- PR description should link to the library
CHANGELOG.mdand call out that this fixes the autosave-polling kludge.