The wiring that makes the menu and OS clipboard work end-to-end. The
protocol bump touches every exhaustive match on the wire enums, so the
daemon / frontend / GPU consumers all land together.
Protocol v11 (additive; SUPPORTED = [6..11]):
- `PointerKind::Context` (right-click), `FrontendEvent::MenuPointer`
(GPU->daemon navigation, index-only), `InstanceMessage::MenuPrompt` +
`MenuPromptRow` (daemon->GPU rows + highlight, daemon-gated >= 11).
Dispatch + producer:
- `EditorState`: menu interception in `dispatch_key`/`dispatch_mouse`,
`MenuKey`, `dispatch_menu_key`/`_mouse`, `open_context_menu` (TUI) /
`open_menu_at_byte` + `dispatch_menu_pointer` (GPU), `build_menu_rows`
(calls the Lua resolver), `dispatch_idle` now false while a menu is
open. `dispatch_pointer` gains the `Context` arm.
- daemon: routes `Context` -> open, `MenuPointer` -> navigate; gates
`MenuPrompt` >= 11; drains the clipboard publish as
`InstanceSignal::Clipboard`; honors the previously-dropped
`FrontendEvent::Paste` (so paste works for the first time).
- `semantic_render`: `MenuPrompt` producer with cached-compare.
Frontends:
- TUI (`frontend.rs`): OSC 52 clipboard write; ignores `MenuPrompt`
(the cell overlay renders the menu).
- GPU (`pmacs-gpu`): `arboard` dep; clipboard write/read + Ctrl-V inbound
paste; right-click -> `Context`; `MenuLocal` + `MenuPrompt` handler;
the popup (a second `TextRenderer` over bg quads) at the click pixel;
hover/click -> `MenuPointer`; key intercept while open.
Also folds a pre-existing clippy `unnested_or_patterns` nit in a search
test (`Color::Indexed(11 | 3)`) that newer CI clippy surfaced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Carries regex mode to the GUI status band and lets the GUI start a
regex search.
SearchPrompt gains `regex` + `invalid` (protocol v10; SUPPORTED grows
to [6,7,8,9,10]). The fields changed that variant's encoding, so the
daemon's per-session gate moves from >= 9 to >= 10 — a v9 peer
negotiates v9 and is simply sent no SearchPrompt (the decorations
still highlight) rather than mis-decoding the wider shape. The
producer fills both from the active SearchSession.
GUI: `is_search_entry_chord` also forwards C-M-s / C-M-r (Ctrl+Alt) so
a regex search can start; M-r (the toggle) already round-trips via the
intercept path once a search runs. The status band reads
`Regex I-search:` in regex mode and `[invalid]` when the pattern won't
compile. Multi-line regex matches needed no GUI change —
push_glyph_extent_rects already fans a byte range across lines.
Tests: SearchPrompt postcard round-trip extended to regex/invalid
shapes; protocol version pin 9→10 + ladder grows to v10; GUI entry
chord accepts C-s/C-r and C-M-s/C-M-r. (last_search_prompt's 5-tuple
factored into a SearchPromptFacts alias to satisfy type_complexity.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings incremental search to pmacs-gpu, which has no minibuffer, by
reusing the shared daemon-side search core from the previous commit.
Key routing needs no new mechanism: `dispatch_idle` now also reports
false while a search is running, so the GPU's existing M11.6
optimistic-apply gate round-trips every keystroke to the daemon —
where `dispatch_search_key` extends the query / steps — instead of
self-inserting it. The match highlights were already wired (commit
2's SearchMatch / SearchMatchActive decoration colors), so they
light up live the moment keys round-trip.
The one thing a semantic frontend can't derive locally is the query
text, so a new additive `InstanceMessage::SearchPrompt { buffer_id,
query, active, total }` carries it (protocol v9, SUPPORTED grows to
[6,7,8,9]). The producer emits it cached-compare-suppressed like
StatusFacts — `query: Some` while searching, `None` to clear on
accept/cancel (matches keep highlighting via decorations), and
stays silent on a fresh buffer that never searched. The daemon's
per-session filter keeps the variant off wires negotiated < 9. The
GPU mirrors it into the status band: while searching, the band's
left side shows `I-search: <query> (n/m)` (or `[no match]`) in
place of the buffer name, returning to the name when the search
ends.
Tests: protocol version pin + SearchPrompt postcard round-trip
(active / failing / cleared shapes); producer emit-on-change +
suppress + clear-on-accept + first-sight silence; dispatch_idle
flips false during search (the GPU round-trip contract).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wire-authoritative half of the status band (Q#S1): an additive
InstanceMessage::StatusFacts { buffer_id, name, modified,
diag_errors, diag_warnings }, emitted by the semantic producer on
change (cached-compare). Counts freeze at their last value while
the diag store is stale — positions go wrong mid-edit but counts
merely lag, and flickering to zero per keystroke would be worse.
The daemon's write loop keeps the variant off wires negotiated
< 8, the DispatchIdle gate shape; SUPPORTED grows to [6, 7, 8].
GPU side: the band's left shows name + modified dot, the right
gains severity-colored E:n/W:n ahead of the local L:C/scroll
readout (rich-text spans, change-detected per side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PointerKind::TripleDown — the cheap additive bump shape returns:
PROTOCOL_VERSION 7, SUPPORTED [6, 7], the new variant kept off
pre-v7 wires by a frontend send-gate that downgrades it to the
plain Down a third click produced before. The GPU's click history
deepens to a chain count (1 → Down, 2 → DoubleDown, 3 →
TripleDown, then restart). Daemon side, select_line_at_cursor
selects the line including its trailing newline, so consecutive
triple-click lines abut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
M4.6 follow-up piece 2. `Style` gains `underline_color: Color`
(Default = follow the text color) so a diagnostic squiggle can be
red/yellow/cyan/gray without clobbering the syntax color of the
text it underlines — exactly why error_style() left its 'red'
unwired until now.
The wire consequence: Style rides inside Cell / CellDelta /
Snapshot / StyleSpans, so this is the protocol's first
encoding-breaking change. PROTOCOL_VERSION 5 → 6 and
SUPPORTED_PROTOCOL_VERSIONS narrows to [6]: postcard is not
self-describing, so no per-session send gate can keep a v5 peer
decoding v6 cells — a mismatched pair now fails the handshake with
a clean VersionMismatch instead of garbling mid-session. Version
policy tests rewritten to pin the new contract.
Surface wiring:
- diag.rs: per-severity underline_color (indexed 1/3/6/8).
- frontend.rs: kitty-style CSI 4:N for Double/Curly/Dotted/Dashed
(previously flattened to plain SGR 4) + SGR 58:5/58:2 emission.
- ansi.rs: parse SGR 58/59 with the 38/48 extended-color grammar.
- overlay.rs merge_styles: non-default-wins, like fg/bg/underline.
- lua_bindings.rs: underline_color on Lua style tables.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per docs/pmacs-gpu-mouse-framing.md (resolves the deferred Q#B5):
a pixel frontend cannot express the daemon's cell coordinates —
inline adornments shift visual columns invisibly to cell space and
the design contract forbids hit-test round trips — so the frontend
hit-tests locally and ships source-byte gestures.
- protocol v5: FrontendEvent::Pointer { buffer_id, byte, kind, mods }
with PointerKind { Down, Drag, Up, DoubleDown }. Double-click
detection is frontend-side (only it knows pixel proximity).
SUPPORTED_PROTOCOL_VERSIONS gains 5; the send gate runs in the
frontend (an older instance cannot decode the variant).
- daemon: dispatch_pointer replays the existing mouse gesture
semantics in byte space against the semantic session's window —
Down places + anchors, Drag grows, Up collapses an empty click,
DoubleDown selects the word. Routed by the authenticated source
(CrdtOp/Viewport trust rule); hit bytes clamp + snap to UTF-8
boundaries (a hit can race an in-flight edit).
- word_range_at fix (pre-existing CUA bug the new test surfaced):
double-clicking a word's FIRST character selected the previous
word too — backward_word from pos sees the non-word char behind
the hit and crosses over; walk from pos + ch_len instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attach-mode optimistic-apply layer (M10.10) classifies any
plain-char keystroke as `Insert(c)` and applies it directly to the
local CRDT mirror, bypassing the daemon's keymap dispatcher. The
documented limitation ("the optimistic layer doesn't track keymap-
prefix state") also covered the minibuffer-active case, which
surfaced during session-5 manual validation: characters typed into a
`C-x C-f` prompt were optimistically inserted into the previously-
active document instead of routed to the minibuffer.
The fix is a daemon→frontend wire signal indicating whether the
daemon's *next* key event would be intercepted (minibuffer or pending
prefix) vs would self-insert. The frontend gates the optimistic-apply
path on this; when not idle, every keystroke round-trips as
`FrontendEvent::Key`.
Protocol changes (pmacs-protocol):
- `PROTOCOL_VERSION` 3 → 4; `SUPPORTED_PROTOCOL_VERSIONS` adds 4.
- New `InstanceMessage::DispatchIdle { idle: bool }`.
Daemon (`src/editor.rs`, `src/daemon.rs`):
- `EditorState::dispatch_idle()` — true iff `dispatcher.pending`
empty AND `minibuffer.is_active() == false`.
- Per-tick emission: `last_dispatch_idle_sent: HashMap<FrontendId,
bool>` tracks the last-broadcast value per session; emission fires
on first frame after attach (absent entry) and on transitions.
- Gated on `crdt_replica` AND `negotiated_protocol_version >= 4` so
older peers don't hard-error on the unknown variant. Same gating
shape as the M10.5 CrdtOp and M11.1 SemanticFrame bumps.
Frontend (`src/attach.rs`):
- New `dispatch_idle: bool` (cfg `crdt`); default `false`
(pessimistic — optimistic apply only activates after the daemon
explicitly says idle).
- DispatchIdle messages consumed in the drain loop; they don't
participate in `present_messages` batches.
- Optimistic-apply branch gated on `dispatch_idle`. When false, the
branch returns false (forces fallthrough to the round-trip
`forward_event` path).
Tests:
- `editor::tests::dispatch_idle_*` — fresh, prefix-pending, prefix-
resolved, minibuffer-open/cancelled.
- `protocol::tests::dispatch_idle_round_trips_through_postcard` —
wire encoding both polarities.
- `protocol::tests::protocol_version_is_four_for_dispatch_idle` +
`supported_protocol_versions_includes_one_through_four` — pin the
new version constants.
Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1474 (+5 from 1469 baseline) with crdt; 1312 (+4) without;
m4 83; m11_5 (--features crdt) 2.
Acknowledged remaining gap: plain-char Lua bindings (e.g. binding
`q` to a command) still surface optimistic-apply divergence —
optimistic doesn't know "is this char bound to a non-self-insert
command in the current keymap." Rare in practice; revisit if anyone
hits it. Documented at session-5 finding time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The big move that completes session 1. Wire types moved from
src/protocol.rs to pmacs-protocol/src/message.rs:
- Input event family: Key, Modifiers, KeyEvent, MouseButton, MouseKind,
MouseEvent, FrontendEvent (and its variants — Resize, KeyEvent,
MouseEvent, Resume, Pause, Detach, ResizeAck, CrdtOp, Viewport).
- Instance-side message family: CursorState, InstanceSignal,
GoodbyeReason, InstanceMessage (Hello/Cursor/CellDelta/CursorByte/
CrdtOp/BufferSnapshot/Goodbye/PresenceUpdate + the SemanticFrame
variants).
- SelectionSnapshot.
- SemanticFrame family components: StyleSpan, StyleSegment,
DecorationKind, Decoration, DecorationSegment, AdornmentPlacement,
AdornmentContent, InlineAdornment, BlockAdornment, ResourceBody.
- Handshake: PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS,
is_supported_protocol_version, InstanceIdentity, InstanceCapabilities,
FrontendCapabilities, NegotiatedCapabilities, negotiate_capabilities,
Hello, AttachRequest.
What stays in src/protocol.rs:
- AttachTarget / AttachError / AttachTargetParseError /
AttachTargetValidationError / AttachTargetError / AttachmentHandle
(CLI / binding internals, not wire).
- crossterm_translate submodule (the crossterm ↔ pmacs-protocol-types
translation layer; sits at the binding boundary, not on the wire).
- Existing tests (wire-format roundtrip + AttachTarget + crossterm
translation), unchanged — they reach the moved types through the
'pub use pmacs_protocol::*' re-export.
Mechanical rewrites inside the moved chunk: crate::buffer::BufferId →
crate::BufferId, crate::rope::Position → crate::Position,
crate::rope::CrdtOp → crate::CrdtOp (the message module is inside
pmacs-protocol; identity types live at the crate root).
Feature re-added on pmacs-protocol: 'crdt' (was removed in commit 3
as I'd thought CrdtOp was the only feature-gated thing — but
InstanceCapabilities::default and FrontendCapabilities::default both
call cfg!(feature = 'crdt') for their multi_frontend / crdt_replica /
semantic_render defaults). Re-added with a doc comment explaining why.
The parent pmacs crate's 'crdt' feature now activates
'pmacs-protocol/crdt' so the cfg!() check evaluates consistently in
both crates.
Full gate green: fmt, clippy --all-targets -D warnings, lib 1314,
m4_acceptance 83, m8_1/m8_9/m8_10 10/26/19, m9_1 18, m5_8 5,
m11_5_semantic_acceptance --features crdt 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workspace skeleton: root Cargo.toml becomes a workspace with members
[".", "pmacs-protocol"]; [workspace.dependencies] pins serde,
postcard, thiserror so both crates use byte-identical versions (the
wire format depends on it). pmacs main package keeps its existing
shape (no file moves); it just gains pmacs-protocol as a path
dependency.
Identity types moved: BufferId (from buffer.rs), FrontendId + ByteRange
(from protocol.rs), Position type alias (from rope.rs). All four are
self-contained — no custom-type dependencies — so the first stage of
the move can land atomically without dragging cell/message types along.
src/buffer.rs / src/protocol.rs / src/rope.rs each gain a 'pub use
pmacs_protocol::...' re-export for the moved names, so existing
internal imports (crate::buffer::BufferId, crate::rope::Position, etc.)
continue to resolve unchanged. New consumers (pmacs-gpu, debug tools)
will depend on pmacs-protocol directly.
One visibility change: BufferId::from_raw was pub(crate); promoted to
pub with a doc note that it's not stable API for external consumers.
The (crate) restriction was advisory only — external deserialization
already worked via the derived Deserialize, so making it pub doesn't
widen the actual surface, just makes it honest.
Lib gate: 1314 passed, no regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New InstanceMessage::FileStyleSummary { buffer_id, generation, lines:
Vec<Style> }: a coarse whole-file styling summary for a Zed/VSCode-
style minimap, resolving the design note's Open Q#2. One dominant
Style per source line (by byte count across the producer's current
spans); the frontend maps minimap rows to one or more lines.
Producer scoped_file_summary reuses scoped_style_spans with a whole-
buffer synthetic viewport, so policy A's authority pick (tree-sitter
for grammar-backed languages, LSP semantic tokens otherwise) is
inherited automatically — no separate styling path. file_style_summary_msg
is keyed on the buffer's CRDT generation: an idle buffer at the same
generation pays nothing (the whole-file summary is the expensive bit
on large files, so re-emit only after edits). First frame for a
buffer always emits; the existing first-frame test updated to expect
3 messages (StyleSpans + Decorations + FileStyleSummary).
Per-line dominant style is the v1 representation. Future refinements
(fixed-N bands; whole-file RLE style runs) are recorded in the design
note as straightforward extensions if a real frontend prefers them.
Structural gating same as the other semantic families: the daemon
only constructs a SemanticRenderState for sessions that negotiated
semantic_render, so non-semantic sessions never receive it. Grid TUI
adds the variant to its ignore list. Round-trip fixture covers it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the read-only LSP feature set (everything except edits).
Same request→store→Lua pattern as the nav batch; these three need
new parsing (new response shapes), so they were deliberately split
from the Location-family PR (#16).
- src/symbol.rs: one flat Symbol type for both symbol requests.
from_lsp_value handles BOTH LSP shapes — hierarchical
DocumentSymbol[] (flattened with depth + parent chain) and flat
SymbolInformation[]/WorkspaceSymbol[] (location.uri, range
optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs
Workspace(query)) so an outline and a query don't collide.
- src/document_highlight.rs: range + DocumentHighlightKind (absent
defaults to Text=1 per spec), (server,uri)-keyed.
- lsp.rs: three ResponseRoute variants + absorb arms + request
methods. documentSymbol/documentHighlight ranges convert via the
requested-doc codec; workspace/symbol results are cross-file →
route uri "" → non-destructive passthrough (same rule as
cross-file definition).
- Lua: raw bindings + pmacs.document_symbol / .workspace_symbol /
.document_highlight read surfaces (the new LSP Symbol is aliased
to avoid the pre-existing project_index::Symbol name clash);
lsp.lua wrappers + an lsp.document-symbols command on C-c o
(modeline summary; outline buffer is future UX).
- Tests: 6 parser unit tests (hierarchical depth/parent, flat
SymbolInformation, range-less WorkspaceSymbol, highlight kind
default, scope non-collision) + an e2e driving all three through
the async bridge asserting shape correctness.
Also includes a pre-existing rustfmt normalization of the #15
semantic-frontend files (protocol.rs / semantic_client.rs /
semantic_render.rs) — main was not rustfmt-clean there after the #15
merge; bundled here per operator decision so the fmt gate is green.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the M11.2/M11.3 coarse whole-payload suppression with a
CellDelta-style diff lifted from positional cells to byte-anchored
ranges.
- protocol.rs: StyleSpans/Decorations refined to
{ buffer_id, generation, full: bool, segments: Vec<...Segment> }.
New StyleSegment{range,spans} / DecorationSegment{range,decorations}.
full=true → frontend discards prior state for the buffer; full=false
→ replace styling only within each segment's range, bytes in no
segment keep prior state. Decorations gains generation for parity.
Each segment carries ALL current items intersecting its range
(clipped), so an unchanged span overlapping a dirty range is
reconstructed. ResourceOffer stays an honest stub (no producer).
- semantic_render.rs: LastFrame baseline per buffer (viewport region
+ full item set). full on first frame / viewport-region change;
else symmetric-difference the ordered sets, coalesce changed ranges
into maximal disjoint dirty intervals, emit one segment per
interval with current items clipped to it; suppress when no dirty
interval. Independent baselines for styling vs decorations.
Byte offsets cascade on edits (an insert shifts later spans), so an
incremental post-edit frame dirties [edit, viewport_end) — bounded;
no-edit frames (cursor/scroll/selection-only) still cost nothing.
10 semantic_render tests (full-on-first/viewport-change, incremental
dirty intervals, independent suppression, unchanged-overlapping
reconstruction) + updated protocol round-trip. Lib (1397 crdt / 1242
non-crdt) + integration green both flavors; clippy -D warnings clean
both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first real producer of the semantic-frontend arc. The instance
projects syntax styling to semantic_render sessions without
rasterizing to a cell grid.
- src/semantic_render.rs: SemanticRenderState, sibling of
instance_render::RenderState. Reads the same EditorState, emits
InstanceMessage::StyleSpans (tree-sitter spans via the active
Theme), scoped + clipped to the FrontendEvent::Viewport byte range.
Emits nothing until a viewport is declared; suppresses
byte-identical frames (per-span delta encoding deferred to M11.4).
- CrdtState::version_scalar(): oplog version vector summed to a
monotonic non-decreasing u64 — the StyleSpans.generation anchor.
- daemon dispatcher: semantic_states map parallel to render_states;
projection selected per session. Semantic sessions get StyleSpans +
CursorByte + BufferSnapshot + CrdtOp + presence, never CellDelta /
grid Cursor. FrontendEvent::Viewport consumed (routed by
authenticated source). SessionEstablished body extracted to
handle_session_established (clippy 100-line ceiling). Grid-less
sessions no longer panic the _ => apply_event arm.
- InstanceCapabilities default semantic_render flipped to
cfg!(feature = "crdt") — the "M11.2 enables semantic" moment,
analogous to the M10.8 Day-4 flip. M11.1 negotiation test comment
updated for the flip (frontend-side default still false keeps M10.7
outcomes unperturbed).
Lib (1392 crdt / 1237 non-crdt) + integration suites green on both
feature flavors; clippy -D warnings clean on both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First milestone of the M11 semantic-frontend arc
(docs/semantic-frontend-protocol.md). Wire-format scaffolding only —
no producer or consumer; mechanically identical to the M10.5 CRDT
wire declaration, and non-breaking by the same slice-membership +
per-session-filter argument.
- PROTOCOL_VERSION 2 -> 3; SUPPORTED_PROTOCOL_VERSIONS [1,2,3]. v0.1
and v1.0 binaries keep connecting unchanged (membership, not
strict equality).
- semantic_render capability bit on FrontendCapabilities,
InstanceCapabilities, NegotiatedCapabilities (#[serde(default)];
instance default false until the M11.2 projection seam).
negotiate_capabilities AND-combines it and enforces the
semantic_render => crdt_replica dependency as a CapabilityMismatch
(a semantic session is also a text replica), never a silent
degrade. PMACS_INSTANCE_SEMANTIC_RENDER env override added.
- InstanceMessage SemanticFrame family: StyleSpans, Decorations,
InlineAdornments, BlockAdornments, FoldState, ResourceOffer.
FrontendEvent::Viewport. Supporting types: ByteRange, StyleSpan,
Decoration/DecorationKind, InlineAdornment/AdornmentPlacement/
AdornmentContent, BlockAdornment, ResourceBody. All byte-anchored;
no pixels cross the contract boundary.
- Grid TUI (frontend.rs) and daemon apply_event drop the new family
silently — the "declared, not yet wired" posture CrdtOp held
between M10.5 and M10.8. Stale v1.0 version-pin tests updated to
the v1.1 truth; negotiation matrix + postcard round-trips added.
Lib + integration suites green on both the default and crdt feature
flavors; clippy -D warnings clean on both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the optimistic local-edit-application layer on top of the M10 CRDT
foundation: frontend-side rope replica with local edit application,
daemon-authoritative broadcast, and bidirectional cursor reconciliation.
Keystrokes feel instantaneous because the local replica answers next-render
queries before the daemon round-trip completes, while the daemon remains
the single source of truth for conflict resolution and broadcast to remote
replicas.
Architecture beats:
- BufferMirror (src/buffer_mirror.rs) holds a per-frontend rope replica
with explicit cursor-staleness tracking. Every event that may move the
active cursor or swap the active buffer marks the mirror stale; the
next CursorByte from the daemon clears it.
- CrdtOpOrigin {OptimisticReplica(FrontendId), DaemonKey} routes broadcast.
OptimisticReplica skips re-application on the originating frontend
(already applied locally); DaemonKey broadcasts to all replicas including
source -- covers Lua-driven and generated-buffer edits that bypass the
optimistic path.
- Generated buffers (*help*, *workers*, *pmacs-instance*, *errors*) funnel
apply_edit output through queue_daemon_origin_crdt_op so post-attach
CRDT upgrades don't drop their edits.
- forbid(unsafe_code) preserved throughout; loro 1.12 added as the CRDT
engine.
Audit posture: M10.10 shipped through six post-audit review rounds with
twenty-eight cumulative findings, most categorized as "incomplete
application of a prior round's mechanism." The audit doc records
grep-driven exhaustiveness as the standing countermeasure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>