Did what removing and re-adding a teammate does match the people design we wrote down? Eight clauses, each quoted from the design and checked against the code — judged on the code the fix was written against, then re-read against that fix as merged.
Re-read against the merged fix Read-only · eight clauses judged 2026-10-16, re-read 2026-10-17 · issue #20464 · fix #20497 · sibling to the remove + re-add inspection
Three of the eight hold, two break, one the design routes around, and two it never answers. That is the code the fix was written against; it has since merged, and the last two sections re-read it as shipped. Removing a teammate ends their job exactly as written. Re-adding them by work email made a second record for the same human, which the design forbids in plain words. The on-call list keeps naming someone who left, which the design forbids in plain words. One rule — that the same phone or the same email inside one company means the same human — the code routes around rather than breaks. And on two points — whether someone who leaves keeps their phone and email, and what adding them back is supposed to do — the design simply says nothing. Silence is recorded here as silence, not filled in with a guess.
Why this page exists. The inspection page states four of these verdicts and cites nothing. Every verdict below names the sentence it is judged against and the line of code that keeps or breaks it, so anyone can check it instead of taking it.
The design is two documents, and only two. Everything judged below is quoted from one of them. Nothing on this page is judged against a preference somebody remembers.
| Source | What it is | What it governs here |
|---|---|---|
| The portfolio architecture, section 06 and the worked examples | The people and calendars picture, plus the two worked examples about a person who leaves a company. | What leaving a company means, and what happens to the lists that name a person. |
docs/architecture/person-lookup.md, the "Identity rules" section | The canonical rules for when two records are one person. | How many records one human may have in one company, and what is allowed to join two of them. |
The four words. Fits the code does what the clause says. Violates the code does the opposite of what the clause says. Not addressed the clause is written but its conditions never come up on this path, or the behaviour sits outside what the clause covers. Silent neither document has a sentence about this at all — which is a finding, not a gap to fill with a guess.
Where the code was read. Every line reference below was opened on origin/main at e31954c3c40 (16 October 2026) in the main application repository. A line number proves somebody looked; the sentence beside it is what that line says. Nothing was run against production for this page, and nothing was written anywhere.
What is deliberately not here. No customer name, no person's name, no record identifier. The incident that prompted this is the one in the inspection page and issue #20464; the facts of it are not restated.
Each clause is quoted first, then checked. The summary is the table; the eight paragraphs under it carry the reasoning and the exact lines.
| # | The clause, in short | Verdict | In one sentence |
|---|---|---|---|
| 1 | One person record per human per company | Violates | Adding someone back by their work address makes a second record for a human the company already has. |
| 2 | Same phone or same email in one company is the same human, and we join them | Not addressed | The rule only fires on an address that is still live, and removing the person retires the address first. |
| 3 | A teammate who is also a resident is one record, and removing them releases only the work address | Fits | The code does exactly this — but only when the person adding them types a mobile number. |
| 4 | Leaving a company means ending the job | Fits | Removal ends every job and the company membership, and writes nothing else. |
| 5 | Ending the job ends their assignments, so the on-call list never pages a departed employee | Violates | Removal touches no list, and the thing that dials at 2am never asks whether the person still works there. |
| 6 | Whether a leaver keeps their phone and personal email | Silent | Neither document has a sentence about contacts on departure for an ordinary teammate; the code retires them. |
| 7 | Two records are joined only on a proven match, never on a resemblance | Fits | The only join on this path matches a real phone record and then checks the names agree. |
| 8 | What adding back a person you removed is supposed to do | Silent | Neither document contains a sentence about re-adding anyone. |
"we cannot always tell at first that the emailer, the caller and the texter are one human, but the moment we can prove it, we join them — one record and one conversation per person per company."
"One Person per company. … Identity is (person, company): contact lookups are company-scoped, and no merge or join ever crosses companies."
docs/architecture/person-lookup.md, Identity rules, opening line and rule 1
Today. Adding someone back looks for an existing record by their email address and nothing else, unless a mobile number was typed into the form. That lookup reads only live addresses — a retired one is filtered out of the results by design, and the paragraph above the code says so in its own words. Removing the person retired that very address a minute earlier. So the lookup misses, and a brand-new record is created for a human the company already has on file.
There are two gates, not one. Even when the address does match, a staff invite refuses to adopt a record the property system owns — the case it was written for is a staff member's work address appearing as the contact on a company-held lease — and makes a fresh record instead, leaving the address where it was. Anyone judging a fix has to clear both gates: finding the record is necessary, and being allowed to use it is the second half.
Evidence. link-user-to-spine.ts:355 (the email lookup, skipped entirely when a phone matched) · persons.ts:1485 ("historical lookups continue to resolve to the same Person … but findPersonByClaim filters deprecated rows out of results") · teammate-membership.ts:566-567 (the removal retires the address) · link-user-to-spine.ts:376-385 (the second gate, and the reason written above it).
"Same phone or same email within one company = same human. When a writer hits a claim already held by another Person in the same company, the two are merged automatically — no human review."
docs/architecture/person-lookup.md, Identity rules, rule 3
Today. This rule is the safety net that would have caught clause 1, and it never gets the chance. It fires when a new record tries to take an address another record is already holding. Removal retires the address first, which hands it back, so the new record takes a free address and no collision ever happens. The rule is not broken; it is routed around. That is worth separating from a violation, because fixing the rule would change nothing — the order of events is what defeats it.
Evidence. persons.ts:1482-1500 (retiring an address frees its uniqueness key in the same database transaction, deliberately, so the value can later land on a different person) · teammate-membership.ts:561-567 (removal does the retiring).
"A staff invite whose mobile is the PRIMARY phone of a Person in the same company links the new login to that Person … instead of minting a second, role-anchored record beside the PMS-anchored one. … The work email goes on as a non-primary claim marked
staffContact, so the PMS import's primary email and phone are untouched … and removing the teammate releases only it."docs/architecture/person-lookup.md, Identity rules, rule 3, the resident-teammate exception
Today. The code does precisely what this paragraph describes, down to the last clause: a person whose record the property system owns keeps their mobile and their personal address when they are removed, and only the work address goes. The catch is reach, not correctness. This join runs only when a mobile number was typed into the add form; with only an email, the join is never attempted. So the one clause written for exactly this situation is implemented, and sits unused on the common path.
Evidence. link-user-to-spine.ts:225-276 (the join, and its refusals) · link-user-to-spine.ts:226 (returns nothing when no mobile was given) · admin/users/route.ts:438-440 (the mobile is handed to the join only when the form carried one) · teammate-membership.ts:561-562 (the primary contacts of a record the property system owns are protected from the retire) · teammate-membership.ts:566-567 (the work address is not).
"She leaves Bellwether: end the role; the cascade ends her assignments in one transaction. Park West's rows do not change."
"She leaves: end the role, and the cascade ends her assignments."
Portfolio architecture, worked examples E3 (a person who works for two customers) and E7 (an agent invited before the first building exists)
Today. The first half holds exactly. Removal reads every job the person holds at that company — including one whose own clock has already run out, deliberately — ends each of them, then ends the company membership. It writes nothing else about the person. The second half of the sentence, the cascade, is clause 5.
Evidence. teammate-membership.ts:175-222 (endTeammateMembership: jobs first, then the membership row, then an optional pointer repoint for a login that survives at another company).
"ending a person's role ends their assignments in the same transact, so the on-call list never pages a departed employee."
"Assignments … ended with the role, in the same transact."
Portfolio architecture, section 06 (People and calendars) — the High reading, and the label on the assignments box in Figure 11
Today. Removal writes to no list of any kind. The emergency on-call list, the hand-off chain and the tour-host list all keep naming the person who left, and the code that dials at 2am reads the person's phone number and never asks whether they still hold a job. If their number has been retired it falls back to the copy stored on the list itself, so the phone rings either way. The only place that notices is the settings screen: the next time somebody saves that list, the save is refused because a name on it is no longer on the team — which is how this surfaces, as an edit that will not go through rather than as a call that should not have been made.
The design's own test for this clause does not exist. The architecture names a check called role-end-cascades as the proof of this sentence, and defines it as marking the departed person "out" in every live schedule row in the same transaction. Searching the whole application repository for that name returns only planning documents — never a test, never a line of code. As a control, a second named check from the same page, second-claim-refused-by-store, returns the same thing: planning documents only. So the sweep ran, and the honest reading is that this family of checks is design-stage across the board, not that this one was singled out.
Evidence. resolve-handoff-phone.ts:82-129 (dials the person's live number, falls back to the stored copy, no job check anywhere) · transfer-numbers-settings.ts:421-448 (the save-time refusal: "isn't on this company's Team") · teammate-membership.ts contains no reference to any on-call, hand-off or tour-host setting — zero matches for all four names, against a positive control on the same file that does match (deprecateClaim, at line 567) · docs/planning/portfolio-architecture/how/design-final.md:981 (the definition of role-end-cascades, in the architecture's own cited source).
Neither document has a sentence about this. The architecture's two departure sentences (clause 4) name the job and the assignments and stop. The identity rules speak to contacts on departure exactly once, in the resident-teammate paragraph quoted at clause 3, and that paragraph says the work address is released. For an ordinary teammate — somebody with no resident record and no other job at the company — there is no written rule either way.
Today that person's mobile and personal address are retired along with the work one, on the reasoning written into the code: if nothing else at the company owns those contacts, they were only ever the login's. That is a defensible reading of silence. It is not a rule anybody wrote, and this page will not promote it to one.
Evidence. teammate-membership.ts:562 (the test for "nothing else owns these") · teammate-membership.ts:565-567 (what gets retired).
"An inbound contact that matches no verified anchor mints a NEW person/prospect. It is never attached to an existing one because circumstance made it look likely. … A missed attach costs a duplicate row that a later verified anchor merges; a wrong attach is unrecoverable and leaks one person's data to another. The asymmetry is the whole argument."
"The same name is never, by itself, the same person."
docs/architecture/person-lookup.md, Hazards, and Identity rules, rule 2
Today. The add path obeys this to the letter. The only join it will make is onto a record that actually holds the typed mobile as its main number, and even then it checks that the first and last names on both sides are present and agree, refuses when the typed email already belongs to somebody else, refuses a number marked as shared, refuses when another login already points at that record, and never reaches into another company. Minting the duplicate is the safe side of the asymmetry this clause describes — which is why clause 1 is a real failure and not a dangerous one.
Evidence. link-user-to-spine.ts:225-276 (the match and its five refusals) · link-user-to-spine.ts:360-374 (a record living in another company is refused by name, never shared).
Neither document contains a sentence about re-adding anyone. The architecture's worked examples follow people in (an invite) and out (a departure) and never back in. The identity rules say how many records one human may have and what may join two of them, which constrains the answer but does not give it: they do not say whether adding a person back should search records of people who used to work there, nor whether a retired address is still proof of who somebody is.
That silence is the whole reason this incident was possible, and it is the one thing on this page that a decision has to fill rather than a fix. It is recorded here as a question, not answered.
The inspection page's section 5 asserts four verdicts in plain words with no citations. Three of the four survive citation. One does not, and the difference matters for the fix.
| Inspection page says | This page says | Why the difference |
|---|---|---|
| Removal ends the role — fits | Fits (clause 4) | Agreed, now with the sentence and the lines behind it. |
| Removal also releases the work email — violates; his model never touches contacts on departure | Silent (clause 6), and for the resident-teammate case Fits (clause 3) | The sentence being cited does not exist. Neither source says contacts survive a departure. One of them says the opposite for the resident case, in so many words: "removing the teammate releases only it." |
| Re-add makes a second record for the same human — violates one person per company | Violates (clause 1) | Agreed, and the rule is quotable: "one record and one conversation per person per company." |
| On-call list keeps naming a removed person — violates | Violates (clause 5) | Agreed, and the clause says it almost word for word. Worth adding: the design's own named test for it was never built. |
One question is live between the two founders, and this page deliberately does not answer it. It is recorded so that whoever does answer it is answering the real thing.
Both readings are reasonable and they are not the same answer. Neither of the two design sources settles it: the architecture describes what removal does to assignments and never describes a re-add at all (clause 8). It is being put to the founders separately.
The scope, in the change's own words. Removing a teammate ends their jobs, their company membership and their sign-in, and releases no phone or email at all. Adding someone looks for a former teammate of that same company: one record holding the typed email exactly — live or released — that holds or once held a job there, or carries that address as its work address. As merged, the email only. A typed mobile is matched by the step that was already there, which demands that the full name agree too; the new step has no name check, so giving it the phone would have joined a new hire to the tech whose company mobile they inherited. Exactly one match, with no other sign-in already pointing at it, gets the job back. More than one match is logged and never guessed. Nobody matches: a new record, as today. No name matching anywhere in the new step, and never across companies. The refusal to adopt a record the property system owns is lifted for this one path — a former teammate who is also a resident is an allowed match — and kept everywhere else. One change, live on merge, no switch. Named as outside it: the "out" marker on on-call slots, duplicate detection, any change to folding two records together, and the repair of the records the incident already made.
| Clause | Under the chosen fix | Why |
|---|---|---|
| 1 · One record per human per company | Fits | Both gates are cleared: the add finds the record whether or not its email was retired, and the refusal that would have made a fresh one anyway is lifted for exactly this path. As merged, by the email. A re-add by the phone reaches the same record through the older name-gated step — which the removal change is what unblocked, since that step reads only a live claim and the old removal had retired it. |
| 2 · Same contact = same human, join them | Fits | Not by firing the rule, but by removing the thing that routed around it: the address is never handed back, so the add sees it. |
| 3 · Resident teammate: one record, and removal releases only the work address | Violates as written — and the change says it rewrites the clause | The rewrite landed in the same change, as the rules document demands of itself — rule 3 no longer says removal releases the work address; it says removal keeps every contact. So the design moved with the code rather than being left contradicting it. One catch, found by this re-read: the new wording promised more than the code it shipped with (the invite's "email or phone", a lookup that runs "first", "any" released claim re-minted), because it was written before that last narrowing commit. Corrected in #20582. |
| 4 · Leaving ends the job | Fits | Unchanged, and this is the half the fix keeps. |
| 5 · The on-call list never pages a departed employee | Not addressed | Out of scope by the change's own words. Nothing in it gives the dialling code a job check, so a removed person still rings — no worse than today, and no better. |
| 6 · Contacts on departure | Silent — the change picks a reading | With nothing written either way, keeping every contact is a choice the silence permits. It is the right choice for clauses 1 and 2; it is still a choice, and it should be written down rather than inferred later from the code. |
| 7 · Joined only on a proven match | Fits the letter — and the change names the case that tests it | The match is on a real address or number this company once proved, never on a name: the clause's own line that a name is never by itself the same person is honoured, because no name matching is added. The clause's deeper argument is that a wrong join is unrecoverable, and the change names the case that tests it in its own words — a company that hands a departed person's work mailbox to a new hire will find the departed person. Three things stand between that and a wrong join: the record must have held a job at this company, there must be exactly one match, and no other sign-in may already point at it. Whether that is enough is the judgement call this clause asks a reviewer to make; it is not a rule breach, and this page does not make the call. As merged, one of the two risky cases is closed by construction. The handed-on mobile cannot reach this step at all — it matches the email only — so what remains is the handed-on mailbox, which is the case the change names in its own words and the narrower of the two. |
| 8 · What re-adding is supposed to do | Silent — the change answers it in code | Answered in the rules document now, not only in code. Rule 3 of person-lookup.md states what a re-add does: removal ends the job, the membership and the sign-in and keeps every contact; the add finds the one former teammate of that company holding the invite's email, live or released, and gives the role back. That is what this clause was asking somebody to write down, and it is written where the next author is sent to read it. |
Two more things the change leaves out, by its own account. First, the inspection page's fourth shipping decision — that folding two records together should also move the sign-in, the company membership and every on-call or hand-off entry that names the old record — is named as out of scope. That is the piece that makes a later clean-up safe, and today folding two records together moves neither the sign-in nor the membership nor any setting that names a person. Second, the change says the path for somebody who still works for a second company skips the new lookup entirely, so a remove-and-re-add at one of two companies can still split a record. Both recorded as not addressed, neither as a complaint: they are separate items on the inspection page's own ship-order list.
Evidence for that last paragraph. merge-persons.ts:154-180 (the four steps a fold performs: contacts, jobs, the rows readers look up by person, and a redirect — and no fifth) · the same file contains no reference to the sign-in row, the membership row or any settings key.
Sibling page: Teammate identity: why remove + re-add made two people — the read-only inspection of the incident, its blast radius, the options and the industry comparison. This page judges; that page describes.