Repository navigation
test(crm): run the CRM relationships scenario through the built CLIs - #315
Merged
Merged
Conversation
leemour
added a commit
to WireCatLabs/cli-core
that referenced
this pull request
Nov 8, 2026
… once (#2664) ## Task Let the owner see and undo merges of people and split a person, and fix duplicate people after a merge: the CRM acceptance run (WireCatLabs/cli-testing#315, check 1) found three Alices after the owner linked her CRM contact and Telegram identity. ## Why - Exact-email matches merge people automatically, so every merge must be undoable, and the owner needs to split a person whose identities do not belong together. - `mergePerson` moves every identity and leaves the emptied person row in place. `taskPeople`, which memo's `people list` and (in memo's CRM branch) `search people` read, still listed that empty row, so one person showed two or three times. ## What No store migration, no new column. Everything is read from `identity_link_events`. - **What counts as a merge** is judged by what the move did (`mergeSql`, `merged-persons.ts`): - A merge empties its person: every other identity on it then moves the same way. An unlink leaves the rest behind. - `unlink`, `split` and `undo-merge` moves are never merges. - `unlinkIdentity` now records method `unlink`. Older unlinks (`manual`) are told apart by the same outcome rule. So is a merge into the owner's person, which has no move of its own. - **`PeopleStore.merges(uid)`** groups the identities now on the person by the merge that brought each one there. An undo that brought an identity back restores the merge it had before. Each group has a stable id `merge:<to>-<from>`, with the identities, the method, who did it and when. `parseMergeId`, `mergeId` and the type `PersonMerge` are exported. - **`PeopleStore.undoMerge(to, from, by)`** is owner only (`permission_error`, `identity.owner-only`). - It moves the identities of that merge that are still on `to` back to `from`, whose row was never deleted. - It leaves alone identities the owner split or unlinked off since. - A chain of merges undoes newest first. - It writes owner events, so the person-level separation guard keeps the people apart afterwards. - It refuses with `conflict` / `identity.stale` when an identity was merged on into another person. - It answers `not_found` when nothing of the merge is left. - **`PeopleStore.split(uid, identities: IdentityRef[], by)`** is owner only. It moves the chosen identities to one new person, with owner events. It refuses an empty list, all of the identities, or an identity that is not this person's. - On both: `by` is `"owner"` only from a path that established the owner ran it. No CLI or MCP command calls them yet. - **A merged-away person** has no identity, has a merge out of it, and is never the owner. - `taskPeople` skips it and counts its tasks for the person it is now. It resolves only merged-away people a task is assigned to, and counts in one grouped pass. With 5,000 emailed CRM contacts (5,000 merged-away persons) it takes 21 ms; the old query took 11 ms. - `personByUid` (with `mergedInto`), `personSeenInAccounts`, name resolution and the mention drain follow its latest merge. - Rows naming it are not rewritten, so undo loses nothing. The readers that filter by a person take `idsOf` (the person plus everyone merged into them): the memories list and search, the facts list, the notes list and annotations, relationships (`relations`, `listRelations`), the imported documents' `person:` filter, the task board's `assignee:` filter and `preparation`. - Assigning a merged-away uid to a task the current person already holds adds no second assignment. - `syncReported` withdraws what a source reported on a merged-away person once it reports the relationship on the current one, so it shows once. - **Account purge**: - A kept merged-away person also keeps the identities that were merged out of it (not ones unlinked or split off), so it stays merged away. - A merged-away person nothing points at is deleted with the account. - Docs: the contacts section in `docs/dev/ARCHITECTURE.md`, and the method notes in the schema reference. ## Testing `src/store/person-merges.test.ts`, 17 tests on fresh sandbox stores: - undo restores the uid and the identities; - the same address coming in again does not rejoin them after undo; - merge, undo, merge again, then undo again; - a chain undone newest first; - undo refused after a merge on into a third person; - a merge into the owner of a person with several identities is listed and resolves; - undo after a split moves only what is still there; - nothing to undo once the owner unlinked it all; - an old `manual` unlink is not a merge; - undo and split are owner only, plus the split refusals; - split, then the same address does not rejoin them; - the CRM scenario gives one person in `taskPeople`, and a name-only person stays listed; - old uids resolve through `personByUid` and `personSeenInAccounts`; - a memory, fact, note and task written about the old uid stay on it and are read for the current person (lists, board filter, task count); - `preparation` reads rows written before the merge; - a source's relationship reported again on the merged person shows once; - a document linked to the old uid shows under the current person's `person:` filter; - assigning the old uid to a task the current person holds is a no-op; - a purge keeps a referenced merged-away person hidden and resolving, and drops an unreferenced one. Also run locally: 37 store and service suites covering facts, knowledge, notes, memories, relations, task board, tasks, purge, separation, same email, mentions, persons, CRM and private people (all green), plus `pnpm typecheck`, `pnpm lint` and `pnpm docs:check`. The full suite runs in CI. ## Performance I ran a one-off script, not committed: `openStore` on a temp file, then `savePeople` for 20,000 Telegram identities, so 20,001 persons. The owner then made 5,000 `linkIdentities` merges (25,000 events, 5,000 merged-away persons), and 200 tasks were assigned. Each figure is the mean of 3 warm runs on an AMD Ryzen AI 9 HX 470 with Node 24.19. | Read | ms | | --- | --- | | `taskPeople({ limit: 5000 })` (memo `people list` / `search people`) | 71 | | count of merged-away persons (the rule on every person row) | 9 | | `mergedAssignees` (pairs for `taskPeople`) | 2 | | `personByUid` of a merged-away uid | 0.24 | | `merges(uid)` | 0.03 | | `accounts.purge` of the whole 20,000-identity account (one-off) | 272 | `EXPLAIN QUERY PLAN` of `taskPeople`: - Every correlated subquery of the merge rule is an index search. It uses `identity_links_by_person_id`, `identity_link_events_by_from_person_id` and `_by_to_person_id` (`rowid<?`), the covering `identity_link_events_by_identity_id` (`rowid<?` / `rowid>?`) and the integer primary key. - The task counts are materialized once (`MATERIALIZE counts`, then `SCAN c LEFT-JOIN`). - The only scan is `SCAN p` over persons, which the list needs anyway. No result is over 200 ms except the one-off whole-account purge, so I made no change. ## Limits - Not changed to read merged-away rows yet: - the task board's assignee facet counts, which still list an old uid apart; - the links list (`notes.links`); - person context's other parts; - ref summaries, which name the old person. - A meeting-participant or mention link that moves one identity out of a person with others is not a merge, so `undoMerge` does not list it. `split` covers that case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The owner asked for one source-backed picture of clients and their people. This adds the plan's acceptance scenario (TASK-925, plan section 8): Acme and Northstar, synced by b24's real binary from a strict fake Bitrix24 portal, then read through memo's real binary and stdio MCP server. - portal.mjs is a fetch preload that answers only documented REST methods, parameters and fields, and refuses anything else. - Telegram, mail and calendar rows come in through cli-core's store calls, as an import makes them. - The owner's memo writes go through a util-linux pseudo-terminal. memo takes a write as the owner's only from a terminal. - Paths are the only inputs (install prefix, docs checkouts, an optional build description), so a rerun on released versions needs no edits. - Versions, lockfile integrities and executable SHA-256 are recorded. The first result is a pre-release run on unreleased local builds (cli-core 11 of 13 checks pass. Two fail on cli-core bugs: - Merged-away persons stay listed in people list (person-links.ts:165). - An owner role edit on a CRM-reported link stays "reported" (relation-proposals.ts:131). Not measured: live Bitrix24, OAuth, a real Zoom recording, identity matching beyond exact email, scale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run now uses cli-core #2642 20cc10fb0 (with #2641 6c411e0eb), merged locally with #2632 e04a396bb; bitrix24-cli #39 66137f596; and cli-memo #699 981c50d97 on #698 e41cbabda. The docs examples are checked against the updated memo docs. 12 of 13 checks pass. Check 10 now passes: the owner's role edit is its own confirmed link. Check 1 still fails on cli-core's mergePerson leaving emptied persons behind (person-links.ts:165); now that search people finds people with no identity, those leftovers show there too. It stays failing until the Memory/Search stream's fix lands. Check 11's control for "Grace is found without a lock" now asks for at least one result. The duplicate person it also finds is check 1's subject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run now uses cli-core #2642 977d0ae29 (with #2641 2c946fa2b), merged locally with #2632 37e93c7f2; bitrix24-cli #39 66137f596; and cli-memo #699 c583358cf on #698 760d846bd. Check 10 follows the new rule: an owner's role edit on a CRM-reported link makes one override that hides the CRM row. After the edit and two syncs, it asserts: - Alice reads only `manager:confirmed` at Acme. - There is exactly one override link, left unchanged. - No proposals or links churn. - Bob's owner-set decision-maker role is untouched. 12 of 13 checks pass. Check 1 still waits for the Memory/Search stream's mergePerson fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0.9.0 and memo 0.131.72 12 of 13 pass. Check 1 passes now. Check 11 fails: an unattended memo search all lists a deal memo's saved permissions deny (fixed in cli-memo #777). The runner gives the docs' 'relationships update 4' examples the person's place on a project, since an earlier example of the page adds one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
leemour
marked this pull request as ready for review
November 9, 2026 18:31
leemour
force-pushed
the
test/crm-relationships
branch
from
November 9, 2026 18:31
1674106 to
e8f54b2
Compare
leemour
added a commit
to WireCatLabs/cli-memo
that referenced
this pull request
Nov 9, 2026
…ls and leads (#777) ## Why [cli-testing #315](WireCatLabs/cli-testing#315), rerun on the published memo 0.131.72 and b24 0.9.0, failed its check 11: with `"deals": "deny"` in memo's saved permissions, an unattended `memo search all` (an agent's shell, no terminal) returned `deal:1`, while `memo deals context` refused the same deal. `src/search/denied.ts` says search all, the contexts and `relationships list` leave such kinds out (SEC-151); search all only checked the MCP server's levels. ## What - `lanesFor` takes the CRM kinds memo's saved permissions deny and hides them, with the denying key in `issues`. - `search all` passes them for every run that is not the owner at a terminal (`whoRuns`). The owner's own search keeps finding everything (#739, `src/crm.test.ts` "whatever the permissions say" still passes). - Changelog: Unreleased → Security. ## Testing - New test in `src/mcp/crm-search.test.ts`: a command-line run with `deals: deny` saved returns only the lead and names the denial; it fails without the fix. - `pnpm typecheck`, `pnpm lint` (8 warnings, same as main), `pnpm docs:check`, full vitest: 132 files, 1317 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
leemour
added a commit
that referenced
this pull request
Nov 11, 2026
…task phrases (#358) ## Task Rerun the CRM relationships scenario (#315) on released b24 and memo, and measure cli-core's new tools search (cli-core #2878) before and after. ## Why - Check 11 failed on 2026-11-09 (a denied deal showed in an unattended `search all`). Its fix, cli-memo #777, shipped since. - Check 12 found that `memo_tools_search` finds only 6 of 13 commands when an agent words a task; #2878 changes the ranking. ## What - `performance/crm/relationships`: rerun on b24 0.13.0 and memo 0.131.84 passes **13 of 13**; check 11 holds. Three runner fixes, none a product bug: choose the CRM user whose deals b24 copies, the fake portal answers `user.admin`, and the owner keeps one contact that memo now hides as test data (cli-core #2862). - `performance/agents/tools-search`: 13 short and 13 task phrases over a real stdio `memo mcp serve`. Task phrases found **6 → 13 of 13**, first place 6 → 9, top 3 6 → 10; short phrases unchanged (13, 9, 12). The after run used local builds (memo branch on cli-core main 0.643.0, unpublished); the README says to rerun it on the release. - Linked from `performance/agents/README.md`; CHANGELOG entries under Unreleased. ## Testing - Both scripts ran end to end in temp folders with no keyring and no real account. - `pnpm lint`, `pnpm docs:check`, rumdl on the changed pages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Task
PR 9 of the CRM relationships plan (TASK-925, plan §8). It adds an end-to-end synthetic acceptance check that drives the built CLIs from the outside: b24's binary, memo's binary and memo's stdio MCP server.
Why
The owner asked for one picture of a client and its people that every answer backs with a source. The cli-core, bitrix24-cli and cli-memo PRs each test their own part. This check asks whether the parts make that picture together, and whether the docs' examples run as written. It feeds the decision to release the stack.
What
performance/crm/relationships/. The README covers why it was run, the exact commands, the results, what it showed and what was not measured.run.mjsruns the Acme/Northstar scenario and the plan's 13 checks, and writes each check's evidence toresult.json.portal.mjsis a strict fake Bitrix24 portal, preloaded into b24's real binary as afetchhook, behind the shared external-I/O guard.src/author.ts). So the owner's memo writes run in a util-linuxscriptpseudo-terminal.workis the Bitrix24 account and the work Telegram account.personalis the personal Telegram account and the mailbox. Check 11 runs each lock both ways, with a control: every item a lock hides is found without the lock.performance/crm/README.mdis added and linked fromperformance/README.md. There is a CHANGELOG line under Unreleased.This is a pre-release run on unreleased builds, on the final heads. None of them were published:
977d0ae29(contains #26412c946fa2b) and feat(groups): account groups for memo's group word (WC-ACCT-13) cli-core#263237e93c7f2. Conflicts were in imports and the CHANGELOG only. It was never pushed.66137f596.c583358cf, on feat(groups): the group word, MEMO_GROUP_LOCK and memo groups (WC-ACCT-13) cli-memo#698760d846bd.Testing
node performance/crm/relationships/run.mjs <prefix> <result.json> --memo-docs <dir> --b24-docs <dir> --build <file>: 12 of 13 checks pass.search people Alicefinds three persons after her identities join. cli-core'smergePersonmoves the identities and never retires the person it empties (src/store/sqlite/person-links.ts:165). The code is the same onmain. It waits for the Memory/Search stream's fix, and the check stays as it is.contactrow. After two syncs, Alice reads onlymanager:confirmedat Acme, there is still one override link, and nothing churns.Also found:
memo_tools_searchneeds every word of a query to appear, so "who reports to whom" finds nothing.Other checks run:
biome checkis clean.rumdlwith the repository config is clean.pnpm docs:checkpasses.Not measured: a live Bitrix24 portal, OAuth, a real Zoom recording, identity matching beyond exact email, and scale.
Rerun on the published packages (2026-11-09)
b24 0.9.0 and memo 0.131.72 on cli-core 0.619.0, from npm: 12 of 13 pass. Check 1 passes now. Check 11 fails on a real gap: with
"deals": "deny"saved, an unattendedmemo search alllists the denied deal whiledeals contextrefuses it. Fix: cli-memo #777. The runner now fills the docs'relationships update 4examples with a real person-on-project link.🤖 Generated with Claude Code