Repository navigation
Collaborative script editing: Save/Stop/Discard, bulk edit (Phase 3, Step 4) - #1480
Conversation
… Step 4, part 1) Groundwork for Step 4 before touching CollabScriptEditor.vue itself: - Brand LineUid/PartUid types (yjsSnapshot.ts), threaded through draftWriter.ts/useDraftWriter.ts and the draft adapter — a Step 3 register item that never actually landed, now that bulk edit gives it a real multi-call-site need. Zero behavior change, caught by typecheck alone. - scriptDraft.ts: canSaveDraft getter, fixing a real bug found while grounding the plan — isDraftDirty unconditionally resets on the initial sync (yjsSync step 0), which is correct for a fresh join but wrong for resuming a draft kept from a prior Stop Editing session. Falls back to scriptConfig.hasDraft, which is already correct by the time this matters. - scriptDraftValidation.ts: a whole-snapshot validator mirroring utils/show/line_type_validator.py's exact rules (D1's client-side gating, which Step 3's plan text claimed was done but never actually was — it only ever validated a line while its own editor was open). - SaveProgressModal.vue + useAutoSave.ts: extracted from ScriptEditor.vue up front, not after a Sonar failure this time (Step 3's own lesson) — the save-progress modal markup and the autosave timer-scheduling are shared with the classic editor, which now uses both. Pure refactor, no behavior change; full E2E re-verified green after each swap. - Backend: close_active_room now demotes every remaining client's is_editor/is_cutting, not just the caller's own (which is all STOP_SCRIPT_EDIT ever did). Found while wiring Discard: DISCARD_SCRIPT_DRAFT closes the room for everyone in it, but nothing ever demoted anyone else still in that room — they'd stay stuck showing as an editor forever, with no room or draft left to show for it. A real correctness gap this step's own Discard flow needs working, not a backend-hardening nicety. Verified: 1273 backend pytest (+3), 326 Vitest client-v3 (+ new files' own counts), tsc/ruff/eslint/prettier clean, full Playwright chromium green (re-run after both the modal and autosave swaps in the classic editor). The D8 fix, the close_active_room fix, and the validator were each verified by reverting and confirming the new test fails, then restoring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
…-v3-p3-step4-save-stop-discard
…utosave (Phase 3, Step 4, part 2) Adds the test coverage the prior commit's Save/Stop/Discard/autosave wiring in CollabScriptEditor.vue was missing: canSave/saveDisabledReason, the Save button's click behaviour, the three-way Stop Editing flow (immediate stop when clean, Keep Draft, Discard with/without other editors present, Keep Editing), the unsaved-draft indicator, and autosaveTick's own gating. Each new assertion was verified by reverting the corresponding guard and confirming the test fails. Also gives the test file's BModal stub no-op show()/hide() methods (previously a bare passthrough element), since exercising Save and the new Stop Editing modal is the first time this test file actually calls them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
Reuses BulkEditModal.vue as-is (presentational, unchanged) and ports the classic
editor's bulk-apply loop onto useDraftWriter. Per D6, the selected start/end lines are
stored as {page, lineUid}, not indices, so a concurrent insert/delete elsewhere can't
silently retarget the range; both are resolved back to a position only at apply time,
and the whole edit aborts with a toast if either boundary line is gone by then. The
range-apply writes share one reentrant doc.transact(..., LOCAL_EDIT_ORIGIN) so the
whole bulk edit reaches peers as a single update. No page-preloading is needed (the
whole draft doc syncs on join), unlike the classic editor's per-page REST loads.
Adds Vitest coverage for the new glue (selection ordering, act/scene and character
application across a range including a page boundary, skipping a line with no part at
the chosen index, and the vanished-boundary-line abort), each verified by reverting the
guard it covers and confirming the test fails.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
…discard bugs it caught Extends e2e/tests/15-collaborative-script-editing.spec.ts with Save, Stop/Keep-Draft/ Discard, bulk edit, and multi-editor-discard coverage for Phase 3 Step 4 — the first time this suite exercises any of that UI. It caught two real backend bugs, each fixed with its own Python regression test: - `_op_save_script_draft` never broadcast `GET_SCRIPT_CONFIG_STATUS` after a successful save (every other lock-lifecycle op already does), so the "Unsaved draft" indicator never cleared after a real save even though the save itself succeeded. - `discard_room`/`discard_active_room` deleted the draft file/row, then called `close_active_room()`, whose own "checkpoint if still dirty" step immediately wrote the in-memory (still-dirty, still-discarded) doc straight back to a brand new draft file — silently undoing the discard it was just asked to do. Fixed by marking the room checkpointed before closing it. Also fixes the suite's final "Stop Editing returns to Edit" test, whose precondition (an immediate stop with nothing unsaved) Step 4's new Keep/Discard modal would otherwise break — resolved by ordering the new tests to leave the draft clean before it runs, not by changing the test itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
Client Test Results102 tests 102 ✅ 0s ⏱️ Results for commit b803c7f. ♻️ This comment has been updated with latest results. |
Client V3 Test Results375 tests 375 ✅ 3s ⏱️ Results for commit b803c7f. ♻️ This comment has been updated with latest results. |
Python Test Results 1 files 1 suites 1m 29s ⏱️ Results for commit b803c7f. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (chromium)243 tests 243 ✅ 2m 34s ⏱️ Results for commit b803c7f. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (firefox)243 tests 243 ✅ 2m 24s ⏱️ Results for commit b803c7f. ♻️ This comment has been updated with latest results. |
Tim020
left a comment
There was a problem hiding this comment.
Review round 1
Reviewed at head SHA 08cfc4d22932e1a22b4585a3ee968918d8d8e278.
Verdict: changes needed. The SonarCloud quality gate is failing and there are several real correctness bugs in the new Save/Stop/Discard and bulk-edit code, concentrated around error handling on failure/race paths rather than the happy path (which is well covered by the new tests).
SonarQube
Quality gate: FAILED — one condition out of five fails:
| Metric | Actual | Required | Result |
|---|---|---|---|
| New duplicated lines density | 4.64% | ≤ 3% | FAIL |
| New reliability/security/maintainability rating | A | ≤ A | pass |
| New security hotspots reviewed | 100% | 100% | pass |
All 89 duplicated new-code lines are in 3 files, 6 blocks:
CollabScriptEditor.vue:744-764⇄ScriptEditor.vue:744-764(20 lines) — the autosave-wiringwatch/onMountedboilerplate; worth a shareduseScriptEditorSetupcomposable, but note only ~5 of these lines are actually new in this PR (the rest is pre-existing context in both files), so this block isn't independently fixable without touching unrelated code.CollabScriptEditor.test.ts(69 of the 89 lines, self-duplicated across the Stop-Editing and bulk-edit test blocks) — extracting shared setup helpers here alone brings density to roughly 1.0%, comfortably under the 3% gate.e2e/tests/15-collaborative-script-editing.spec.ts:225-242⇄308-325(shared "add dialogue line" sequence) — candidate for a newe2e/helpers.tshelper.
Fixing the test file is the highest-leverage single change for the gate. 4 Sonar-flagged code issues are posted inline above (2× typescript:S3776 cognitive complexity, 1× typescript:S1128 unused import, 1× python:S7504 redundant list()). No security hotspots were found (0 returned, not an access failure).
New findings (9 inline comments)
- Important (4): unguarded DB commit in
close_active_room()'s new demote block that can leave a room wedged and a discarding client with no feedback; no lock betweensave_room/discard_room/discard_active_room(a real race, not just a test gap);discardAndStop()silently no-ops from the join-failed/idle state; the save-progress modal can show "Finished saving script." when the save never actually sent. - Suggestions (5): two SonarQube cognitive-complexity flags (
onBulkApply,validateDialogue),onBulkApplyreaching pastuseDraftWriter's documented encapsulation with its docstrings not updated to say so, a stale "bulk edit not yet implemented" line left in a docstring this PR itself edited, a non-discriminated-union validation result type, an unused import, and an unbrandedline_typefield that defeats exhaustiveness checking.
Pre-existing bug surfaced during review (not in this PR's diff, so not postable inline)
client-v3/src/stores/scriptDraft.ts's scriptSaved() (around line 421-439, untouched by this PR) computes wasOurSave = this.isDraftSaving. _armSaveWatchdog() (lines 302-311) independently flips isDraftSaving to false after a 30s stall timeout, regardless of whether the save actually completes later. If the real SCRIPT_SAVED for our own save arrives after the watchdog has already fired, wasOurSave reads as false, and the code takes the "someone else's save" branch, clearing isDraftDirty unconditionally even if edits were made during the stall window — those edits would show as "saved" without actually being covered by the save that completed. Worth a follow-up: give each save request its own identity (a token/counter) instead of reusing isDraftSaving, which a timeout can mutate out from under this check. This predates Step 4, but Step 4's new Stop-Editing-disabled-during-save and Save button make the save flow more load-bearing, so it's worth fixing in this area.
Test coverage (not anchored to a diff line — flagging here)
- No test exercises the save-vs-discard race described above (would belong in
server/test/utils/script/test_script_room.py, next to the existing discard/checkpoint regression tests). - The only two-browser E2E test has the second client idle (no concurrent typing, no concurrent CRDT merge check), and the new Discard test's "second editor" has no real unsaved edits of its own — so the PR's own "other editors will lose their work too" warning is only verified as label text in a unit test, never as an actual behavior end-to-end.
- Bulk edit: no test for a line inside the range disappearing mid-apply (only the start/end boundary case is covered), and no test for an empty-selection no-op.
- No test asserts the new
:disabled="draftStore.isDraftSaving"on the Stop Editing button. - The classic
ScriptEditor.vue's autosave/save-modal refactor (swapping to the shareduseAutoSave/SaveProgressModal) has no dedicated unit test, andSaveProgressModal.vuehas no component test of its own.
Strengths
LineUid/PartUidbranded types threaded through the whole Yjs write path are a genuine compile-time win — aPartUidcan no longer be passed where aLineUidis expected, at zero runtime cost.- Bulk edit's
{page, lineUid}boundaries, resolved only at apply time and aborting cleanly if a boundary line vanished, correctly handle concurrent inserts/deletes. save_room()'s existing error handling (separate from the bugs flagged above) is genuinely careful — each risky step has its own narrowtry/exceptwith a specific client-facing error.- The two real backend regressions this PR fixes (discard not reverting dirty content, save not broadcasting a config-status refresh) each get a focused, well-documented regression test.
scriptDraftValidation.tsmirrors the server'sline_type_validator.pyaccurately, including the one deliberate client/server difference, and is itself thoroughly tested.
🤖 Generated with Claude Code
…omplexity, duplication
All 9 inline findings and the two summary-level items from round 1 are addressed here.
Important findings:
- No lock protected save_room/discard_room/discard_active_room/close_active_room
against each other, or against a maintenance-tick checkpoint firing mid-discard —
a real race, reachable by a single editor, not just multi-editor. All four now
serialize on the existing _room_lock; close_active_room split into a public
lock-acquiring wrapper and a lock-free _close_active_room_locked() (asyncio.Lock
isn't reentrant, and get_or_create_room already held the lock across a call to
it). Added a locked checkpoint_if_dirty() helper and routed the two external
pre-close checkpoints and _run_maintenance's periodic tick through it instead of
calling _checkpoint_room directly.
- close_active_room()'s editor/cutter demote block had no try/except — a DB error
there would leave the room wedged in memory, with a discarding client getting no
feedback (the draft is already deleted by that point). Now wrapped, logs and
falls through to ROOM_CLOSED/teardown regardless.
- discardAndStop() set stopRequested unconditionally before the discard was even
attempted, breaking the join-failed/idle recovery path. discardDraft() now has
no isDraftActive guard (its only caller is reachable solely from the editor UI)
and returns whether it actually sent; discardAndStop() only sets the flag on
success. discard_room's no-room branch now also demotes the caller directly, so
a phantom editor with no room can actually recover via Discard.
- saveModal could show "Finished saving script." for a save that never left the
browser (saveDraft() can fail synchronously on a dead socket). saveDraft() now
returns whether it started; the modal only opens on true.
Suggestions:
- onBulkApply reached past useDraftWriter's encapsulation to hold a Y.Doc locally.
Added transact() to useDraftWriter instead, and extracted applyBulkActScene/
applyBulkCharacter to bring its cognitive complexity under the Sonar limit.
- LineValidationResult is now a discriminated union on isValid, making
{isValid:true, error:'x'} and {isValid:false, error:''} unrepresentable.
- validateDialogue split into small predicates for complexity; the unreachable
default branch in validateDraftLine's switch now returns a real invalid() rather
than silently passing an unknown line type as valid (line_type itself stays a
plain number — it's a shared API type also used by the classic editor).
- Dropped the unused parseDbId import and fixed a stale "bulk edit not yet
implemented" docstring line.
Summary-level items:
- Pre-existing bug (flagged by the reviewer, not anchorable to this PR's diff):
scriptSaved() read isDraftSaving to tell our own save from someone else's, but
the stall watchdog resets isDraftSaving on a timeout purely for the UI — a late
reply to our own save could then misread as someone else's and unconditionally
clear isDraftDirty. Added a separate ownSaveInFlight flag, set on send and
cleared only by scriptSaved()/saveError() actually resolving it.
- SonarCloud duplication gate: extracted mountAsEditor()/mountSyncedDraft() helpers
in CollabScriptEditor.test.ts (removes a ~4-line prefix repeated across ~20
tests), and added addDialogueLine() to e2e/helpers.ts for the duplicated
add-dialogue-line sequence in spec 15.
- Added the coverage the review flagged as missing: Stop Editing disabled while
saving, a SaveProgressModal component test, bulk edit with an empty selection
and with a line inside (not at the boundary of) the range vanishing before
apply, and reworked the E2E Discard test so the second editor makes a real
unsaved edit of its own before the discard — actually exercising the "other
editors will lose their work too" warning end-to-end.
Every fix has a new or updated regression test, each verified by mutation testing
(revert the fix, confirm the test fails, restore). Backend: 1280/1280 pytest,
ruff check/format clean. Frontend: 374/374 Vitest, typecheck/lint clean. E2E:
243/243 on both chromium and firefox.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
Round 1 fixes —
|
Tim020
left a comment
There was a problem hiding this comment.
Review round 2
Reviewed at head SHA 8d3b4580bde4e367d9aafa3c0e188dc2fd948ab3.
Verdict: changes needed (minor). This is a strong fix commit — 8 of the 9 round-1 findings are fully and correctly resolved, with real regression tests for each, and the SonarCloud gate now passes. One new, non-blocking correctness bug was introduced by the fix for finding #6, flagged below.
Previous round follow-up
- Verified fixed (7): close_active_room's unguarded demote block now wrapped in try/except, with the redundant
list()(Sonar S7504) dropped too; no lock between save/discard/close now serialized on_room_lockvia a clean locked/unlocked method split that avoids deadlocking the non-reentrant lock;discardAndStop()silent no-op from idle; save modal false-success; stale "bulk edit not yet implemented" docstring; non-discriminated-unionLineValidationResult; unusedparseDbIdimport. - Verified fixed with a reasoned alternative (1):
validateDialogue's complexity split — the author declined the exactLineTypenarrowing suggested (would ripple into the classic editor's shared type) but changed thedefault:branch to reject rather than silently accept an unknown line type, which addresses the actual risk. Accepted. - Fix incomplete (1):
onBulkApply's Sonar complexity — the encapsulation-boundary fix (useDraftWriter.transact()) is fully correct and well-documented, but cognitive complexity is still 16 vs. the 15 allowed (down from 20). Doesn't block the gate; left open. - Needs human decision (1):
BulkEditModal's emit shape was left as five independently-nullable fields rather than the typed union suggested, with the stated reason that it's shared with the classic editor and out of this PR's scope — reasonable, but a project-direction call rather than something to adjudicate in review.
SonarQube
Quality gate: PASSED (was failing in round 1 on duplication; now 2.9% vs ≤3% required — all other conditions pass). 1 new open issue, already covered above: typescript:S3776 on CollabScriptEditor.vue:692 (onBulkApply, complexity 16 vs 15), non-gate-blocking.
New findings
- Important (1): a new
ownSaveInFlightleak via theCOLLAB_ERRORsave-rejection path, posted inline above — introduced by (and only possible because of) this round's fix for the pre-existingwasOurSaverace. - Test gap (not anchored to a diff line): the demote-block try/except fix (round-1 finding #1) has no test — nothing simulates a
session.commit()/session.get()failure inside_close_active_room_locked's demote loop to confirmROOM_CLOSEDstill broadcasts and the room still tears down. Worth a follow-up test intest_script_room.py. - Minor note: the concurrency test for the new
_room_lock(round-1 finding #2) lives intest_collab_editing_mode.pyrather thantest_script_room.py, and proves no-deadlock rather than interleaving correctness — acceptable, not a blocker. - Minor note:
save_room()now holds_room_lockfor the duration of its broadcastwrite_message/ws_send_to_allawaits to every room client, so a slow client can delay joins, discards, closes and maintenance checkpoints for as long as that write takes. Not a bug today (rooms are small), but worth knowing if this ever needs to scale — gathering broadcast payloads under the lock and sending after release would remove it.
Strengths
- The lock refactor (
_room_lock+ locked/unlocked method split) is genuinely well-designed: every caller of the lock-free_close_active_room_locked()was traced and confirmed to already hold the lock, andcheckpoint_if_dirty's re-check ofself._room is room and room._dirtyafter acquiring the lock closes the narrower maintenance-checkpoint race flagged alongside the main one. - Every fix has a real, specific regression test (7 of 9 confirmed non-superficial by independent test-coverage review), several explicitly mutation-tested per the author's own summary.
- The three "additional coverage" items volunteered beyond what was asked (Stop-Editing-disabled-during-save, bulk-edit empty-selection/mid-range-deletion, and the E2E discard test now using a real unsaved edit) are all present and genuinely exercise the scenarios, not just happy-path labels.
- Where the author disagreed with a suggestion, they gave a specific, scoped reason rather than silently diverging or over-applying the suggestion into unrelated code.
🤖 Generated with Claude Code
…lexity, test gap - A rejected save fails via COLLAB_ERROR (not SAVE_ERROR) for both of _op_save_script_draft's own rejection paths (a live session became active, or insufficient write access), but collabError() never cleared ownSaveInFlight — only scriptSaved()/saveError() did. Left stuck true, it would misread a later, different editor's SCRIPT_SAVED as our own, leaving isDraftDirty wrongly stuck true after a rejected save followed by a local edit. collabError() now resolves ownSaveInFlight (and the save UI state) the same way saveError() does, scoped to only run when a save was actually in flight. - onBulkApply's cognitive complexity was still 16 vs. the 15 Sonar allows (down from 20 in round 1). Extracted applyBulkEditToLine() to pull the per-line body out of the double-nested loop, as suggested. - Added the test-gap coverage requested (not blocking, but actionable): a DB failure during close_active_room's demote block previously had no test proving ROOM_CLOSED still broadcasts and the room still tears down — added one that injects a session.get() failure and confirms both. All three mutation-tested. Backend: 1281/1281 pytest, ruff check/format clean. Frontend: 375/375 Vitest, typecheck/lint clean. E2E: 243/243 on both chromium and firefox. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
|
Round 2 fixes — `b803c7f`Replied inline in both threads (the new Also addressed the non-inline test gap you flagged: added a test in Left the three "minor note" items as-is per your own assessment (not blockers): the concurrency test living in Verification
🤖 Generated with Claude Code |
Tim020
left a comment
There was a problem hiding this comment.
Review round 3
Reviewed at head SHA b803c7fb025c45f9759aaf8cc602e8a67f25aba2.
Verdict: ready to merge. Both remaining findings from round 2 are now fully and correctly fixed, with no new bugs introduced. All 11 findings raised across 3 rounds are resolved or settled. SonarCloud is fully clean.
Previous round follow-up
- Verified fixed (2):
onBulkApply's remaining cognitive-complexity issue — theapplyBulkEditToLineextraction preserves identical behavior (same calls, same order, same vanished-line guard), and SonarCloud now reports 0 open issues for this PR. TheownSaveInFlightleak viaCOLLAB_ERROR—collabError()now resolves it correctly scoped to only fire when a save was actually in flight, with a real regression test. - Also addressed (not inline, from round 2's summary-level test gap): a new backend test (
test_a_db_failure_while_demoting_does_not_block_room_closed_or_teardown) injects a DB failure into the demote block and confirmsROOM_CLOSEDstill broadcasts and the room still tears down — verified correct and using the project's standard test-case base class. - Needs human decision, still open (1):
BulkEditModal's emit shape (five independently-nullable fields vs. a typed union) remains a deliberate project-direction call, out of this PR's scope per the author. Not something for this review to adjudicate further. - Noted, not a blocker (1): a narrow residual edge case in the
ownSaveInFlight/collabError()fix — a same-clientYJS_UPDATEapply failure arriving just after a save was sent could still clearownSaveInFlightprematurely, causing a later real save'sSCRIPT_SAVEDto be misattributed. Fully closing this needs the server to sendSAVE_ERROR(notCOLLAB_ERROR) for_op_save_script_draft's own rejection paths. Flagged on the thread as an optional follow-up, not blocking.
SonarQube
Quality gate: PASSED. All conditions green, 0 open issues, 2.8% duplication on new code (≤3% required), 0 security hotspots. Confirmed via anonymous SonarCloud API against the exact head commit.
New findings
None. The round-3 diff (108 lines across 4 files) was reviewed in full; no new bugs found at reportable confidence.
Strengths
- Both fixes were verified not just by re-reading the diff but by checking they don't double-fire against the other code paths that also touch the same state (
saveError(),scriptSaved(),_teardown()), and by confirming the new/existing tests actually pass locally. - The author continues to volunteer regression tests beyond what was strictly asked (the DB-failure demote test from round 2's non-blocking summary note), each mutation-tested per their own process.
- Across all 3 rounds, every fix was either fully correct on the first attempt or converged to fully correct within one follow-up — no finding required more than two rounds to resolve.
🤖 Generated with Claude Code



Summary
Phase 3, Step 4 of the client-v3-only collaborative script editing rebuild (issue #889): save, stop-with-choice, discard, autosave, and bulk edit for
CollabScriptEditor.vue, building on Step 3's line/text editing (#1479, merged).canSave/saveDisabledReasonmirror server validation client-side (D1) so Save disables itself with a specific reason before the round trip. ASaveProgressModal.vue(extracted and shared with the classic editor) shows progress/result.useAutoSavecomposable (also shared with the classic editor), lighter-touch than classic's blocking modal — silently skips a tick if the snapshot currently fails validation, per Tim's decision.scriptDraft.ts'scanSaveDraftnow coversisDraftDirty || scriptConfigStore.hasDraft, fixing a rejoin bug where resuming a kept draft left Save stuck disabled until a fresh edit.BulkEditModal.vueas-is; selection is stored as{page, lineUid}(D6), resolved to a position only at apply time, aborting with a toast if a boundary line vanished; the whole range-apply is one reentrantdoc.transact(..., LOCAL_EDIT_ORIGIN).LineUid/PartUidtypes throughout the draft-writing stack.Two real backend bugs found and fixed by the new E2E coverage (the first time this suite exercises Save/Stop/Discard/bulk edit through the UI):
_op_save_script_draftnever broadcastGET_SCRIPT_CONFIG_STATUSafter a successful save, so the "Unsaved draft" indicator never cleared client-side.discard_room/discard_active_roomdeleted the draft, then immediately re-checkpointed the still-dirty in-memory doc straight back to a new draft file — silently undoing the discard. Fixed withroom.mark_checkpointed()beforeclose_active_room().Both backend fixes have their own regression tests, verified via mutation testing (temporarily reverted the fix, confirmed the new test fails, restored).
Test plan
typecheckandlintclean (client-v3);ruff check/ruff format --checkclean (server)🤖 Generated with Claude Code
https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8