feat: editor polish, version bump & new features (v0.3.0) - #14
Merged
Conversation
Version bump 0.1.0 → 0.3.0 with workspace.package.version. BackTab/Shift-Tab support across terminal, GUI, and PTY paths. Font zoom keybindings (Ctrl+=/-/0) wired to GuiRenderer. :read/:r command for shell output and file insertion. Per-buffer project roots with active_project() fallback. Doom-style status line: git branch, project, file %, AI tier. AI agent launcher (SPC a a) with configurable ai_editor option. KB project nodes from .project files, .project format alignment. Session persistence (:session-save/:session-load, per-project JSON). Vimtutor-style onboarding (:tutor, SPC h t). Sample config template (assets/sample-config.toml). OptionRegistry: ai_editor, ai_tier, restore_session options. README badges updated (1,508 tests), ROADMAP updated. Doom-style init.scm added to Phase 7 M4 roadmap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Project switching: auto-detect and switch project context when opening files from different project roots (Doom Emacs parity). SPC p p palette now accepts typed paths for adding new projects. :add-project and :remove-project ex-commands added. - Config wiring: ai.editor and editor.restore_session now loaded from config.toml and applied on startup. persist_editor_preference supports new keys. - AI tool gaps: editor_state tool now reports git_branch and project_root. refresh_git_branch() called on startup and project switch. - CPU usage fix: TUI loop uses dirty-flag rendering — only repaints when state actually changed. Shell tick reduced from 30fps to 20fps (50ms). sysinfo sampling interval doubled (every ~6s instead of ~2s). - Roadmap updated with variable-height font priorities and KB-based tutor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cuttlefisch
force-pushed
the
feat/editor-polish-v0.3.0
branch
from
April 19, 2026 12:11
1b8cb74 to
c7e27de
Compare
- Tutor: 11 KB nodes replace static string; :tutor opens help with Tab/Enter/C-o navigation and cross-links to concepts/commands - Shell exit: auto-close buffer on process exit (no more blank frames) - Agent shells (SPC a a): tagged, auto-close with distinct status msg - find-file (SPC f f): use active_project_root() with CWD fallback - Shell CPU idle: generation-based dirty tracking (~0% vs 30%) - Empty rope panic: guard set_cursor_from_offset for shell buffers - Debug stats: show FPS instead of frame timing (μs → fps) 1,509 tests, clippy clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix stale test counts (521→1,509), feature checklist, milestone deps - Mark M3 font zoom + BackTab as done, M8 click/scroll done in M3 - Add v0.3.0 items: tutor→KB, shell auto-close, CPU fix, find-file fix - Update next priority order (Phase 7 tutor done, M3 remaining next) - Sync CLAUDE.md with latest completed features Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The event loop was rendering immediately on every event with no frame interval, causing 120fps at idle and ~30-40% CPU even when nothing changes. Now uses Alacritty/Helix pattern: - MIN_FRAME_INTERVAL = 16.67ms (~60fps cap) - First event after idle renders immediately (no input latency) - Rapid events coalesce: render deferred to next frame slot via tokio timer in select! block - Combined with generation-based shell dirty tracking, idle CPU should drop dramatically Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
open-ai-agent (SPC a a) now spawns the agent command directly as the PTY program via spawn_command(), not inside the user's shell. When the agent exits (e.g., user declines trust), the PTY exits immediately and the buffer auto-closes. Previously the parent shell stayed alive after the agent exited, leaving a useless terminal. Also adds ShellTerminal::spawn_command() for running arbitrary commands as PTY programs with proper lifecycle tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Shell PTY augments PATH with ~/.local/bin, ~/.cargo/bin, ~/bin, ~/.npm-global/bin when launched from desktop (no login shell) - set_status() echoes to *Messages* buffer for review via SPC b m - Dashboard buffer type: splash renders on dashboard, not scratch; SPC x toggles scratch, SPC h d returns to dashboard - TUI only marks dirty on key press/resize, not mouse/focus events Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- shell_render: use already-locked term ref for display_offset (deadlock fix) - main.rs: clamp shell_dims_for_buffer to prevent underflow - terminal.rs: defensive spawn validation + theme color API - key_handling: is_splash_visible uses BufferKind::Dashboard - AI tools: render_inspect, theme_inspect, mouse_event, shell_scrollback - Theme: ANSI color export, improved gruvbox-light/solarized-dark - GUI: popup rendering cleanup, shell scrollback rendering - Shell lifecycle: theme-aware env vars, expanded spawn logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…AP attach/evaluate Tier 1: Watchdog Thread + Structured Tracing - watchdog.rs: standalone OS thread monitors main-thread heartbeat via AtomicU64 - After 6s stall: dumps /proc/self/task thread states to log - After 10s stall: captures full backtrace - trace! instrumentation in shell_render, terminal lock sites, shell lifecycle - debug! render branch logging in GUI Tier 2: Live Self-Debug + AI Introspection - lock_stats.rs: global lock contention tracking (acquisitions, wait times per site) - PerfStats: stall_count, jank_count, anomaly_log ring buffer (100 entries) - Self-debug: new Performance and Lock State scopes with live refresh - introspect AI tool: comprehensive diagnostics (threads/perf/locks/buffers/shell/ai) Tier 3: Event Recording - event_record.rs: 10,000-event ring buffer with microsecond timestamps - Commands: :record-start, :record-stop, :record-save <path> - event_recording AI tool: start/stop/status/dump actions Tier 4: DAP Attach + Evaluate + Conditional Breakpoints - dap_attach_with_adapter: attach to running process by PID (lldb/codelldb) - dap_evaluate: expression evaluation in debuggee context - BreakpointSpec: condition + hit_condition support through full stack - dap_disconnect AI tool with terminate_debuggee param - :debug-attach <adapter> <pid> and :debug-eval <expr> commands - DapClient::evaluate(), DapCommand::Evaluate, DapTaskEvent::EvaluateResult 1,517 tests, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Doom-style init.scm: 8 sections (UI, Theme, Options, Keys, AI, Shell, Hooks, Commands), all 14 options documented with defaults - Tutor KB: Lesson 11 (Debugging) + Lesson 12 (Observability), 4 concept nodes (watchdog, event-recording, dap-attach, introspect) - Command registry: debug-attach, debug-eval, record-start/stop/save registered for command palette and help discoverability - --check-config CLI flag: validate init.scm + config.toml without launching the editor (headless, for CI) - CI E2E step: new e2e/check-config job builds TUI binary and runs --check-config to catch init.scm parse errors - Clippy fix: EventRecorder derivable_impls lint (was failing CI) - ROADMAP.md: test count 1484, Phase 4c M4 debugger additions, Phase 7 M4 init.scm marked done, priority order updated - CLAUDE.md: Phase 8 updated with debugger/docs/CI additions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix e2e/check-config CI job: --exclude requires --workspace - Document vertical line color bug in insert mode (GUI) in ROADMAP Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Line numbers/gutter, syntax highlighting, visual selection highlighting, Unicode/glyph fallback (7-level font chain), and click-drag select were all implemented but still marked as "Not yet" in the roadmap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jun 27, 2026
…9, adversarial-testing principle (#159) * docs(security): E2E_ENCRYPTION.md — design-of-record + security review (ADR-037 #155) The design-pass cryptographic review (4 prior-art-grounded lenses: AEAD/key-wrap, key-management/trust/rotation, metadata/N-peer-scaling, prior-art positioning). Verdict: the core crypto is SOUND (no must-fix break in the primitives). MAE v1 (single per-KB symmetric content key, sealed-box-wrapped per member, distributed via the signed membership op-log, rotated on removal) ≈ Jazz/cojson, is more efficient than Signal Sender Keys (O(N) vs O(N²) on removal), and avoids Megolm's homeserver-trust mistakes; BeeKEM is the named FS/PCS + O(log N) evolution (ADR-037 §D4). Documents the threat model, primitives, key lifecycle, prior-art comparison, the honest "what we do NOT protect" list (no FS/PCS — the documented CRDT trade-off; O(N) rekey; metadata: edit sizes/timing/author/social-graph; cleartext manifest titles), and the review findings with disposition. F1/F2/F4/F6 fold into 3b (#151); F5/F7/F8/F9 + mesh anchor pinning tracked in #156; implementation-pass review re-runs after 3b/3c. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(security): SECURITY_REVIEW.md — identity + authorization deep review (#155) Prior-art-grounded design-pass audit of the identity + authorization foundations under E2E KB sharing (companion to E2E_ENCRYPTION.md). Adversary: MITM, malicious key-blind relay, unauthorized peer, removed member, concurrent races. Verdict: identity = sound SSH-style asymmetric trust (RFC-7250 raw-public-key TLS, authorized_keys/known_hosts TOFU, mesh node-id anti-spoof); authz = sound capability+ ReBAC hybrid (UCAN attenuation, Keybase sigchains, p2panda strong-removal, external- anchored signed op-log). NO auth-bypass / NO privilege-escalation found. Findings concentrate at the op-log↔legacy-member_roles boundary + enforcement coverage, sharing one fix (derive role+epoch+blocklist from the one op-log via a shared fence on every write path): - A1 (HIGH): epoch read from legacy member_roles not the op-log → non-epoch-0 mesh members wrongly fenced from editing. - N1 (HIGH): the ADR-023 epoch fence runs only on the hub path, not the mesh dialer. - A2 (MEDIUM): verify_content_op ignores the local blocklist. - N2 (MEDIUM): content-key authority frozen at the genesis owner under quorum. Identity: I1 single-key reuse (sign+TLS+node-id+X25519 wrap), I2 no key rotation/rebind, I3 at-rest plaintext, I4 non-unix chmod, I5 two-layer revocation, I6 TOFU first-contact. Each finding cites file:line + a primary-source prior-art basis (Zanzibar, UCAN, SPKI/SDSI, MLS RFC 9420, RFC 7250, p2panda-auth, Matrix cross-signing, age/SSH). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(principles): add Architecture Principle #14 — adversarial testing, not confirmation Codify the standing testing-rigor directive into the design principles: no fragile linear happy-path tests; no cherry-picked "unicorn" values; favor property/round-trip, N-way convergence, real/varied inputs, selective oracles, and the attacker's negative case that MUST fail (wrong key, forged sig, stale epoch, removed member, hostile relay). Per-phase adversarial review. Adds a matching bullet to the Scheme Testing Framework Design Principles and an adversarial-test clause to principle #9's regression guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(kb-sharing): holistic reference + ADR-038/039 (editor-authored membership; identity+authz hardening) Ties identity → authorization → encryption → the hub/P2P lifecycle into one reference: - docs/KB_SHARING.md — "how it works in the wild": the three-layer model, both transports (one protocol), a worked end-to-end example (Alice enables+shares, Bob joins+approved+keyed, Carol removed+rotated, daemon key-blind throughout), the management surface, and the honest protected/not-protected summary. - docs/adr/038-editor-authored-membership.md — the owner editor authors the signed membership op-log via the key-blind kb/collection_op RPC (daemon stores opaque owner-signed bytes); member pubkey via PendingRequest; dual-write op-log+member_roles. - docs/adr/039-identity-authz-hardening.md — the security-review decisions: one unified op-log fence (role+epoch+blocklist on every write path, #157); signed encryption mode + fail-closed; E2e⇒SingleOwner; anchor pinned to the authenticated owner; identity follow-ups (key rotation/rebind, single-key separation, at-rest, Windows perms, #158). - CLAUDE.md — index ADR-038/039 + the three sharing/security docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 27, 2026
cuttlefisch
added a commit
that referenced
this pull request
Jun 27, 2026
) (#164) * feat(sync): author_rotate_on_remove — content-key rotation on member removal (ADR-037 §D3, #152) The kb.rs authoring core for Phase 3c. On removing a member from an E2e KB, the owner authors ONE combined collection delta: a signed `Remove` of the departed member (+ member_roles mirror with #72 epoch tombstoning), then one owner-authored wrap-only `Admit` per REMAINING member carrying a freshly-wrapped new key. Design: - Re-key ops re-assert each member's CURRENT derived role/can_invite/epoch verbatim (a re-admit overwrites the derived entry — "later re-admit wins", membership.rs:855 — so preserving them avoids a silent downgrade). Attributes come from the authoritative op-log derivation, not the legacy mirror. - Epoch is NOT bumped: re-keying must not force the remaining members to rebase. The removed member is dropped from derived membership, so their stale lineage is refused regardless of epoch (the fence, #157). - The removed member receives no new wrapped op → find_wrapped_content_key returns only their OLD key. They decrypt pre-rotation content but no post-rotation ciphertext. - Daemon stays key-blind: it relays the owner-signed delta via kb/collection_op. Adversarial test (the §D3 SELECTIVE oracle): 3 members share k; remove B with fresh k'. Asserts the two remaining converge on k', the removed B keeps ONLY the old k (not k', not nothing — proving k' is denied specifically, not B's pipeline severed), B is dropped from membership while owner+C retain unchanged role/can_invite/epoch (the clobber guard), a pre-rotation replica applying the relayed delta agrees on all points, and the Remove op carries a verifiable owner signature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(collab): rotate the content key when removing an E2e KB member (ADR-037 §D3, #152) Wires the editor side of Phase 3c onto author_rotate_on_remove. Removing a member from an E2e KB (one with a registered content key) now ROTATES the key instead of issuing a plain daemon-authored kb/remove_member: - The KbRemoveMember intent carries the main thread's cached collection replica (mirrors KbApprove / KbSetEncryption), so the network task — which holds the identity secret + the content key — can author the rotation. - The network task: derives the current members from the op-log, generates a fresh k', wraps it once per REMAINING member (the owner re-keys itself via its own pubkey; others via the pubkey stored on admit, ADR-038), authors the combined signed Remove + per-member re-key delta, ships it KEY-BLIND via kb/collection_op, then persists k' and registers it so the owner's next edits seal under it. - Members with no stored pubkey are skipped with a loud warn (they keep the OLD key until a re-share re-wraps them) — no silent coverage gap (principle #14). - Adds and non-E2e removes fall through to the legacy kb/add_member / kb/remove_member path unchanged. The daemon stays key-blind and re-derives membership (the signed Remove drops the member) from the relayed delta. Data-layer convergence + the §D3 security oracle (removed member stranded on the old key; remaining members converge on k') are covered by the author_rotate_on_remove adversarial test (a9c30e3); the full wired daemon e2e lands in 3d (#153, docker). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 27, 2026
cuttlefisch
added a commit
that referenced
this pull request
Jun 27, 2026
…#170) (#172) * fix(collab): fail CLOSED on E2e KBs — never emit plaintext node updates (CRITICAL #168) build_kb_node_update_request decided seal-vs-plaintext purely on content_key.is_some() with no E2e knowledge, so an E2e KB with a missing key (e.g. after restart, before a share/join response repopulated content_keys) — or a seal failure — shipped PLAINTEXT to the key-blind daemon, which stores + relays it. Silent confidentiality breach. Fix (fail closed): - build_kb_node_update_request takes `e2e: bool` and returns Option: None = REFUSE. Both the no-key arm and the Err(_) seal-failure arm return None when e2e — never the cleartext. - New `kb_collection_is_e2e(collection_state)` reads the AUTHORITATIVE signed derive_encryption (F1 anchor pin), not the relay-flippable unsigned flag — a downgrade can't trick us into plaintext. The editor stamps this onto CollabCommand::KbNodeUpdate.e2e at drain time (the editor thread is the authority; it holds the collection replica). - The handler lazily reloads the persisted content key from content_key_store on an E2e KB with no in-memory key (restart liveness), then refuses + REQUEUES with a loud warn! if still absent — the edit retries when the key arrives (owner: on reload; member: on approve). No silent plaintext, no lost edit; observable via tracing. Tests (principle #14, the attacker oracle): - build_kb_node_update_request_fails_closed_on_e2e_without_key — E2e + no key ⇒ None on BOTH the signed and unsigned paths; selective control: the same inputs on an UNENCRYPTED KB still ship (Some), proving the refusal is the e2e gate working, not a dead function; E2e + key ⇒ Some (seals). - kb_collection_is_e2e_reads_signed_mode_not_the_flippable_flag — plain ⇒ false; signed enable ⇒ true; a relay forging the unsigned flag back to None does NOT downgrade. Sibling leak in the share/re-share path (raw plaintext node snapshots) filed as #170. 117 collab_bridge tests green; clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(collab): fail CLOSED on the share/re-share path — never ship plaintext snapshots (CRITICAL #170) The ShareKb handler sent node_states as RAW PLAINTEXT (update_to_base64(state)). The editor re-shares durably-shared KBs on reconnect, so an already-E2e KB leaked its full plaintext node content to the key-blind daemon every reconnect — worse than #168 (full content, not a delta). Fix (fail closed, principle #8 extraction): - New pure `select_share_node_states(kb_id, e2e, node_states, op_sets)` decides the wire states. On an UNENCRYPTED KB it is byte-identical to before (base64 the plaintext). On an E2e KB it NEVER ships the plaintext snapshot: it sends the already-sealed op-set we hold for the node (idempotent — the daemon stores the same op-set kb/node_update produces) and SKIPS a node we have no op-set for (loud warn), rather than leak its plaintext. - ShareKb computes `e2e = kb_collection_is_e2e(&collection_state)` (the authoritative signed mode, #168 helper) and routes through the new helper. Test (principle #14): select_share_node_states_never_ships_plaintext_on_e2e — E2e ships the sealed op-set for a held node, SKIPS a bare node, and a plaintext canary appears in NO wire payload; selective control: the unencrypted path still ships every plaintext node. Residual (documented separately): content shared BEFORE encryption was enabled stays plaintext on the daemon — retroactive re-encryption-on-enable is a known limitation, not this leak. This fix stops the re-share from RE-leaking post-enable content. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(e2e): record the enforced fail-closed seal (#168/#170) + the re-encryption-on-enable limitation (#171) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(sync): regression guard — a sealed delete is fenced via its outer-op client_id (#167/#168) Proves WHY #168's always-seal closes #167's deletion-fence gap for E2e KBs: a deletion sealed into the op-set rides a client-id-stamped outer op, so update_new_op_authors attributes it to the (stale) seal client_id and the ADR-023 fence rejects it — even though the inner op is a pure, otherwise-unattributable yrs delete. Documents the residual: a PLAINTEXT pure-delete remains unattributable (the unencrypted-path #167 gap). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jun 27, 2026
…r rotated keys to members (HIGH #173) A member derived its per-KB content key ONCE, at join/share/enable. On subsequent kbc: collection broadcasts, handle_kbc_membership_broadcast relearned the epoch but never re-derived the content key, so the network task's content_keys (seal + open) was frozen at first derive. Consequences: - 3c rotation was broken for remaining members: after the owner rotated to k', members kept the old k — couldn't open k'-sealed content, sealed their edits under the stale k → divergence after any membership change. - Wrap-on-admit for a member who already joined never reached their seal/open path. - Subsumes #169 M2 (concurrent rotations converge once both deltas merge + re-derive). Fix: - KbCryptoCtx gains kb_collections: per-KB full collection replicas, seeded at join/share/enable, advanced by inbound kbc: deltas. - New refresh_kb_content_key_on_collection_delta: on a kbc: delta, advance the replica and RE-DERIVE content_keys via derive_kb_content_key (+ persist), wired into both inbound sync_update paths. No-op for non-kbc / unseeded / unencrypted (a plain KB is undisturbed). - Rotation handler now registers the find_wrapped winner derived from the post-rotation collection (the #169 M2 single-source-of-truth fix), not the locally-generated k2. Test (principle #14, the rotation-delivery oracle): refresh_kb_content_key_re_derives_on_rotation_remaining_yes_removed_no — a REMAINING member re-derives k' on the rotation delta; the REMOVED member stays stranded on the old k. 116 collab_bridge tests green; clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jun 27, 2026
…r rotated keys to members (HIGH #173) (#175) A member derived its per-KB content key ONCE, at join/share/enable. On subsequent kbc: collection broadcasts, handle_kbc_membership_broadcast relearned the epoch but never re-derived the content key, so the network task's content_keys (seal + open) was frozen at first derive. Consequences: - 3c rotation was broken for remaining members: after the owner rotated to k', members kept the old k — couldn't open k'-sealed content, sealed their edits under the stale k → divergence after any membership change. - Wrap-on-admit for a member who already joined never reached their seal/open path. - Subsumes #169 M2 (concurrent rotations converge once both deltas merge + re-derive). Fix: - KbCryptoCtx gains kb_collections: per-KB full collection replicas, seeded at join/share/enable, advanced by inbound kbc: deltas. - New refresh_kb_content_key_on_collection_delta: on a kbc: delta, advance the replica and RE-DERIVE content_keys via derive_kb_content_key (+ persist), wired into both inbound sync_update paths. No-op for non-kbc / unseeded / unencrypted (a plain KB is undisturbed). - Rotation handler now registers the find_wrapped winner derived from the post-rotation collection (the #169 M2 single-source-of-truth fix), not the locally-generated k2. Test (principle #14, the rotation-delivery oracle): refresh_kb_content_key_re_derives_on_rotation_remaining_yes_removed_no — a REMAINING member re-derives k' on the rotation delta; the REMOVED member stays stranded on the old k. 116 collab_bridge tests green; clippy clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jul 26, 2026
…ied not rewritten Tackled ahead of Phase C by explicit decision (see this session's stocktaking): Phase D was already well-specified and named as this ADR's own single highest-priority adversarial test (real Gitea CVE-2026-27771/CVE-2026-58444 + Vaultwarden CVE-2026-27898 precedent), while Phase C's stated premise needed correcting first. Same principle-#15 discipline as Phase B: write the adversarial tests against current code before assuming new enforcement needs building. Three named cases, three different outcomes: - IDOR case (this phase's PRIMARY test): a genuine structural property of Phase A's own addressing, not a bolted-on check -- snapshot_query_layer/snapshot_store resolve the instance address to one specific Arc<CozoKbStore> before any inner ID is looked at, so every lookup is backed by only that store's relations. Proved with a real cross-instance ID collision (handler::tests:: idor_a_valid_instance_address_never_resolves_a_different_tenants_id): a node inserted directly into tenant B's store, requested via a validly-addressed tenant A request, resolves Null across kb/get/kb/links_from/kb/links_to, with a third uninvolved tenant C per principle #14's N-way requirement. - Role composition: roles are derived per-collection in collab_handler/mod.rs's kb_access -- an entirely separate daemon listener (mTLS collab) from the KB Unix-socket path Phase A/B touched (which has no principal/role concept at all, by design -- see daemon/src/config.rs's own comments). No existing test proved a principal holding DIFFERENT roles on two different KBs doesn't leak the stronger one across the boundary, so daemon/src/collab_handler/tests/collab_handler_cross_kb_role_isolation_tests.rs (new file) closes that gap: bob, real Owner of his own KB, denied an Owner-only action on a second KB where he's only Viewer; the reverse also verified. - Forged/rotated-key signature: already covered, pre-existing, unrelated to this ADR -- shared/sync/src/membership.rs's tampering_any_field_breaks_the_signature (+ several collab_handler forged-signature tests) confirmed by direct reading, not duplicated. docs/adr/060-daemon-multi-tenancy.md gained a Phase D Implementation Note (same convention as Phase B/C's) plus a Status line update. assets/mae-adr.cozo regenerated. Issue #412 closed, including an honest caveat: the issue's own "regardless of quota headroom" DoD phrasing can't be fully tested yet since Phase C's quota mechanism doesn't exist -- the role-isolation property itself is verified; the quota-interaction half needs re-checking once Phase C ships. Verified: daemon workspace full test suite (cd daemon && cargo test --all-targets) -- 104 bin + 156 lib tests, 0 failed; clippy -D warnings and fmt --check both clean. New tests run 3x locally to rule out flakiness before relying on them (both are deterministic logic tests, not timing-based, so risk was low but checked anyway). Editor workspace (cargo check --workspace --all-targets) unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks
cuttlefisch
added a commit
that referenced
this pull request
Jul 27, 2026
…n-seed diagrams `partition_boundary_links_by_instance` was previously called exactly once, against only the seed's own boundary links (`populate_graph_buffer`'s Multi branch). Each related instance's own boundary links went straight into that diagram as unclassified stubs, so a real link from one related instance to another (neither being the seed) rendered as an ordinary same-instance- looking dashed stub, never promoted to a cross-instance chord. - `mae_kb::CrossInstanceLink` gains `source_instance: Option<String>` (mirrors `KbCrossInstanceLinkInfo`'s existing field), populated by `partition_boundary_links_by_instance` from the `owner_instance` it's called with. - `populate_graph_buffer`'s related-instance loop now calls `partition_boundary_links_by_instance` per related instance too (not just the seed), accumulating cross-links from every rendered diagram. No double-discovery risk: a directed link is a boundary link of exactly the one diagram whose extraction included its source and excluded its target, so concatenation needs no dedup. - `cross_link_infos`/`cross_instance_links_kept` now use each link's own `source_instance` instead of hard-wiring it to the seed. - `describe_state()`'s "Cross-KB links" text now also names the source's KB when it isn't the seed (previously only the target's KB was shown, an assumption that broke once the source can be any rendered diagram). Adversarial tests (CLAUDE.md #14): a 3-non-seed-instance chain (B->C detected with source_instance == B; C->D dropped-with-count via the existing hidden-link mechanism, without corrupting the B->C count); reciprocal A<->B links authored in both directions rendering as two distinct chords, not collapsed; a related instance's own link back to the seed promoted correctly (the original bug's exact shape, reversed); and a `describe_state()` text test confirming both endpoints' KB now appear when the source isn't the seed. All pre-existing Single-mode and Multi-mode regression tests pass unmodified. Verified: cargo build/test/clippy/fmt clean across mae-kb/mae-core/ mae-canvas (2830 mae-core tests passing, 9 new), workspace-wide clippy clean, code map up to date. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jul 27, 2026
… own center flatten_scene_graph_cached computed scene origin (0,0) ONCE for the whole merged multi-KB scene and bowed every Chord-mode edge toward that single point -- correct only when there's exactly one diagram (a lone chord ring is centered at scene origin by construction). After build_multi_kb_chord_positions composes N diagrams onto a grid and re-centers the grid's bounding box onto (0,0), scene origin becomes the grid's CENTROID -- not any individual diagram's own center, which DiagramLabel.center_x/center_y tracks but flatten_scene_graph_cached never read. Fix: broadcast each diagram's own center across its contiguous node range (node_diagram_centers, new pub(crate) helper mirroring node_degrees) and thread it into flatten_scene_graph_cached as a new diagram_centers parameter, looked up by edge.source in the Chord curvature formula. The public 4-arg flatten_scene_graph wrapper (test-only, ~35 call sites) keeps passing an empty slice, which falls back to the old global-origin behavior -- byte-identical for Single mode and single-diagram Multi mode, since that one diagram's center IS scene origin by construction. Two related bugs fixed in the same pass: - is_boundary was derived from edge.style.dashed alone, but cross-instance edges are ALSO dashed (reusing the boundary-stub convention), so they never curved and always got boundary-stub coloring. Replaced with is_self_link (edge.source == edge.target), which correctly separates self-links/boundary stubs (still straight + boundary-colored) from real cross-instance edges (now curve like ordinary internal edges, with normal edge color) -- deliberately NOT describe_state's `dashed && rel_type.is_none()` formula, which would also misclassify a genuine self-link (it too carries rel_type: Some(..)). - GraphStyleOptions::from_editor read the global kb_graph_layout_algorithm unconditionally, but Multi mode always chord-grids node positions regardless of that option. graph_view_reflatten_window now forces layout_algorithm = Chord whenever GraphView.mode == Multi. Adversarial tests added (CLAUDE.md #14): a 3-diagram grid where each diagram's internal edges bow toward its own center, not the centroid; a cross-instance edge between diagonal (non-adjacent) grid cells gets real curvature + normal color; a genuine self-link in a multi-diagram scene still renders as a straight boundary-colored stub (the exact trap flagged during design review); Single mode vs. single-diagram Multi mode produce byte-identical flattened output; Force-global-option + Multi mode still uses Chord-style curvature. Verified each new test actually fails when the corresponding fix is reverted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cuttlefisch
added a commit
that referenced
this pull request
Jul 27, 2026
Opening the KB graph view always creates a 60/40 tiled split. Toggling the fullscreen overlay (`o`) is a pure bool flip that paints the graph over the whole screen, but the original pane is still alive in `window_mgr`, just visually hidden underneath. `Editor::focus_window_at` hit-tested mouse clicks against the stale pre-overlay tiled rects with zero awareness of `kb_graph_view_overlay_active` -- a click landing where the hidden pane used to be silently refocused it, and keyboard dispatch then resolved that buffer's keymap (no exit binding), trapping the user in an unresponsive-looking fullscreen graph. Fold the same `kb_graph_view_overlay_window()` guard already used by the GUI hover and wheel-zoom paths into `focus_window_at` itself (CLAUDE.md principle #8) so all 3 of its call sites (button-press, drag-select, focus-follows-mouse in gui_app.rs) inherit correct behavior from the one shared function, with no per-call-site fix needed. Adds adversarial coverage per principle #14: overlay-off case proves the fix is gated (a genuine tiled-pane click still refocuses it), a 3-window layout proves the overlay preference holds regardless of sibling window count, and an out-of-bounds click proves the existing no-op behavior is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Jul 30, 2026
Merged
cuttlefisch
added a commit
that referenced
this pull request
Aug 3, 2026
…perty test Second silent miss in the same function, this one MAE's own: the post-query verification split the raw query on whitespace and required a literal substring match, so the prefix query `buffer*` produced the single term `buffer*`, which no document text contains -- every candidate the index correctly returned was dropped and the caller saw zero hits. Verified: `buffer*` matched 1 row in `nodes:fts` while `fts_search` returned 0. Terms are now split on `!is_alphanumeric`, mirroring cozo's `Simple` tokenizer, so the guard checks exactly what was indexed. A candidate with no row in `nodes` is still dropped -- that stale-entry case is what the guard exists for. Replace the `fts_search_finds_nodes` unicorn assertion (principle #14) with a property test over a named 10-node corpus: every term in a node's title or body must retrieve that node. Covers multi-word titles, punctuation, case variation, digits, underscores/hyphens, and non-ASCII (NFC accents, CJK+kana, Cyrillic, Greek). Plus a ranking oracle -- a corpus-unique term must rank its own node FIRST -- so a fix that finds everything but orders it uselessly fails too. Confirmed the tests falsify: reverting the separator to ' ' fails 6 of them, naming exactly the title/body boundary terms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 5, 2026
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.
Summary
v0.3.0 branch: editor polish, debugging powerhouse, documentation overhaul.
Editor Polish
Debugging Powerhouse
Documentation
CI
Test plan