toga-ai 1.0.902 → 1.0.903

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
22
22
  - **_underscore** (_Underscore) _(framework core)_ — 90 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
23
  - **worker2** (Worker) — 80 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
24
24
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
25
- - **dbchanges2** (Database Changes) _(framework core)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
+ - **dbchanges2** (Database Changes) _(framework core)_ — 20 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
26
26
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
27
27
  - **saml** (SAML SSO Gateway) — 9 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
28
28
  - **apiproxy** (API Proxy) — 1 doc(s) → [2.0/apps/apiproxy/INDEX.md](2.0/apps/apiproxy/INDEX.md)
@@ -8,4 +8,5 @@
8
8
  | [Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)](features/item-fulfillment-tableviews.md) | 2.0 | Why Compass Canada's item-fulfillment TableViews 500'd on dead Core ids and how they were rebuilt onto the USA shape; open when a Canada TableView 500s on /meta |
9
9
  | [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | Compass Canada's pure per-line fulfilled/partiallyFulfilled rule (ASN bridge only) and why it diverges from USA; open before touching Canada `_fulfillmentStatus |
10
10
  | [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. |
11
+ | [Copying a Compass USA user into the Canada tenant (cross-tenant hand-add)](workflows/copying-a-us-user-into-the-canada-tenant.md) | 2.0 | Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops |
11
12
  | [Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)](workflows/grand-and-toy-asn-backfill.md) | 2.0 | How to recover Compass Canada shipments whose ASN never landed (cXML replay, CSV-to-JSON, or SQL line backfill); open when replaying a G&T outage or repairing a |
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: compass-canada
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-25
21
+ updated: 2026-09-28
22
22
  owners: [jcardinal, bala, tcox, apeterson, ajean, mhammontree, rgirish]
23
23
  files: []
24
24
  related:
@@ -26,6 +26,7 @@ related:
26
26
  - ../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md
27
27
  - features/grand-and-toy-asn-import.md
28
28
  - workflows/grand-and-toy-asn-backfill.md
29
+ - workflows/copying-a-us-user-into-the-canada-tenant.md
29
30
  - ../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
30
31
  - ../compass-usa/features/approval-decision-flow.md
31
32
  - ../compass-usa/features/stranded-approval-reassignment.md
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Copying a Compass USA user into the Canada tenant (cross-tenant hand-add)
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: compass-canada
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-09-28
10
+ owners: [bala]
11
+ files:
12
+ - dbchanges2/Client_CompassCanada/2026-09-28a - AddJamesFullerSuperUser.sql
13
+ - worker2/Worker/Client/Compass/PeopleFile.php
14
+ related:
15
+ - ../profile.md
16
+ - ../../compass-usa/features/people-file-user-lifecycle.md
17
+ - ../../compass-usa/workflows/granting-persona-bundle-access.md
18
+ - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
19
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
20
+ ---
21
+ Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops the next PEOPLE run switching them off.
22
+
23
+ ## Summary
24
+ US and Canada are **separate tenants** (`Client_Compass` / `Client_CompassCanada`). A US person has no Canada row at all, so "give James access to the Canada portal" is a **hand-add**, not a permission change. Two artifacts are always needed together:
25
+
26
+ 1. A guarded, re-runnable `dbchanges2/Client_CompassCanada/<date><letter> - ….sql` that builds the row chain.
27
+ 2. A one-line edit to `ALWAYS_ACTIVE_EMAILS_CA` in `worker2/Worker/Client/Compass/PeopleFile.php`.
28
+
29
+ Skip step 2 and the person is switched off on the next nightly PEOPLE run. The importer routes rows by `erp_system_id` (**1001** = US, **2010** = Canada), so a `@compass-usa.com` person only ever appears on the US side and the Canada inactivation sweep treats the new row as a leaver.
30
+
31
+ Template: `2026-09-28a - AddJamesFullerSuperUser.sql` (James Fuller, `Client_Compass` user id 1851). It mirrors how Gina Prince (CA user 22897) and Kathy Sweda (CA user 22898) were added.
32
+
33
+ ## Steps
34
+
35
+ ### 1. Read the US row, then check Canada first
36
+ Pull `Users` + roles + personas for the person from prod `Client_Compass`. Then **query `Client_CompassCanada.Users` by email before writing anything** — there is no unique key on `Users.email` in either tenant, so the database will happily accept a second account. (Index/collation facts: [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).)
37
+
38
+ ### 2. Insert the chain in this order
39
+ `Client_CompassCanada.Users.contactId` is **NOT NULL**, and `Contacts` has **no email column** — the address lives in `ContactEmailAddresses` (`id, uuid, contactId, name, emailAddress`) and is pointed at by `Contacts.primaryContactEmailAddressId`. So:
40
+
41
+ 1. `Contacts`
42
+ 2. `ContactEmailAddresses`
43
+ 3. `UPDATE Contacts SET primaryContactEmailAddressId = …` (guard on `IS NULL`)
44
+ 4. `Users` (`contactId`, `email`, `c_hrEmpUsername`, `isActive = 1`)
45
+ 5. `Users_Roles` — one row per role
46
+ 6. `Users_Personas` — one row per persona
47
+ 7. optional `UserGlobalSettings` language row (`settingId = 2`, value `'en'` or `'fr-CA'`)
48
+
49
+ Guard every statement (`NOT EXISTS`, `IS NULL`) so the file is re-runnable — pattern: [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md). **Guard the `Users` insert on `email`, not on the new uuid**, otherwise a re-run with a regenerated uuid creates a second account.
50
+
51
+ ### 3. Map roles and personas by **uuid**, never by id
52
+ Role ids differ between the two tenants. Look up `(SELECT r.id FROM Roles r WHERE r.uuid = …)` inline. Prod-verified 2026-09-28:
53
+
54
+ | Role | uuid | US id | CA id |
55
+ |---|---|---|---|
56
+ | Base | `61d79bfb-4182-2c7a-6217-2510d6766bf1` | 1 | 1 |
57
+ | Admin | `0f7b101b-8050-4036-9129-84bc822f60f3` | 4 | 5 |
58
+ | SuperUser | `d11ce7ac-ed88-4f85-aba9-6c2b1d1e09fd` | 5 | 6 |
59
+ | Contractor | `1003f7a1-e798-11ee-b854-0aae2980db55` | 6 | 7 |
60
+ | Compass Base | `22c90415-0179-0445-c782-a02e066e00f9` | 7 | 8 |
61
+ | Manager | `459bbf3b-1eca-c049-040c-c492a6beee0f` | 8 | 9 |
62
+
63
+ `PeopleFile.php` already keeps `ROLE_UUID_BASE` / `ROLE_UUID_COMPASS_BASE` / `ROLE_UUID_MANAGER` constants for the same reason — reuse those values.
64
+
65
+ **Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest catalogue — 213 items, all 35 bundles; this is Gina Prince's) and **Managers/VIPs** id 33, uuid `7961068f-8b31-1daf-5f52-0e45a09beb98` (168 items, 21 bundles). Persona **All** is id 1. Persona grants resolve through three sources, not just `Users_Personas` — see [Granting persona/bundle access](../../compass-usa/workflows/granting-persona-bundle-access.md).
66
+
67
+ ### 4. Add the address to `ALWAYS_ACTIVE_EMAILS_CA`
68
+ In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACTIVE_EMAILS_CA` — **not** `ALWAYS_ACTIVE_EMAILS_BOTH`. The BOTH list's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. `isEmailAlwaysActive()` lowercases its input, so a mixed-case constant entry never matches. Both layers of the guard are described in [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).
69
+
70
+ ### 5. Review and hand over
71
+ Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change, then give the developer numbered run steps (SQL file + environment order, then the worker2 deploy).
72
+
73
+ ## Gotchas
74
+ - **`Client_CompassCanada.Users` has no `username`, `isSuperUser`, `personaId` or `roleId` column.** Access is entirely `Users_Roles` + `Users_Personas`. The login/personnel username is `c_hrEmpUsername`, stored lowercase.
75
+ - **Leave `Users.password` NULL.** Login is SSO on the email address: only **45 of 28,788** `Client_CompassCanada` users hold a password, and the US rows for these people are NULL too.
76
+ - **`Users.email` uses a case-insensitive collation**, so the stored case does not affect matching — `Client_Compass` often stores these addresses uppercase while Canada stores them lowercase.
77
+ - **The Tools Compass admin cannot do this.** Activating a user outside the cron is temporary; the hardcoded constant is the only durable hold. Adding the person there instead of in the constant is the common mistake.
78
+ - **Keep the file inside `Client_CompassCanada`.** Never join `Client_Compass` and `Client_CompassCanada` in one statement — separate prod clusters. Read the US row, then write Canada-only SQL with the values baked in.
79
+
80
+ ## Related
81
+ - [Compass Canada profile](../profile.md)
82
+ - [Compass PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md)
83
+ - [Granting a Compass user a bundle/persona and SuperUser](../../compass-usa/workflows/granting-persona-bundle-access.md)
84
+ - [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md)
85
+ - [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)
86
+
87
+ ## Change history
88
+ - 2026-09-28 — Created from the James Fuller hand-add; records the uuid role/persona map and the `ALWAYS_ACTIVE_EMAILS_CA`-not-BOTH rule (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-28
10
10
  owners: [bala, ajean, mhammontree]
11
11
  files:
12
12
  - worker2/Worker/Client/Compass/PeopleFile.php
@@ -21,6 +21,7 @@ related:
21
21
  - ../profile.md
22
22
  - ../../../2.0/apps/_underscore/features/record-change-audit-log.md
23
23
  - ../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md
24
+ - ../../compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md
24
25
  ---
25
26
  The nightly Compass PEOPLE-file cron's Users-row lifecycle (insert/reactivate/deactivate), why it creates duplicate accounts, and its raw-SQL/index traps; open before touching PeopleFile.php or debugging stranded approvals.
26
27
 
@@ -69,13 +70,15 @@ Within one job the **inserts (and their id refresh) run before the inactivation
69
70
  ### Always-active protection is per-tenant, two layers
70
71
  A small hardcoded set of people must stay active regardless of what HR sends. Since **2026-09-16** that set is split into two class constants on `_Worker_Client_Compass_PeopleFile`:
71
72
  - **`ALWAYS_ACTIVE_EMAILS_BOTH`** — the original four addresses (two `@compass-usa.com`, two `@compass-canada.com`). Held active in **both** `Client_Compass` and `Client_CompassCanada`.
72
- - **`ALWAYS_ACTIVE_EMAILS_CA`** — two `@compassdigital.io` **Compass USA** staff (usernames `shuttk01` / `racerk01` in `Client_Compass`). Held active in **`Client_CompassCanada` only**. They are allowed to go inactive in the US tenant when the PEOPLE file drops them; that is the requirement, not a bug.
73
+ - **`ALWAYS_ACTIVE_EMAILS_CA`** — three **Compass USA** staff held active in **`Client_CompassCanada` only**: `kaylin.shutte@` / `kate-lynn.racer@compassdigital.io` (usernames `shuttk01` / `racerk01`) and, since 2026-09-28, `james.fuller@compass-usa.com` (`fullej01`, US user id 1851). They are allowed to go inactive in the US tenant when the PEOPLE file drops them; that is the requirement, not a bug.
74
+
75
+ **Which list to use is the decision that matters.** A US-only person hand-added to Canada belongs in **CA**, never in BOTH — BOTH's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. Entries must be **lowercase**: `isEmailAlwaysActive()` lowercases its input and compares strictly. Full hand-add procedure: [Copying a US user into the Canada tenant](../../compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md).
73
76
 
74
77
  `isEmailAlwaysActive(string $email, string $tenantKey): bool` is the single decision point — a lowercased match that returns true for the BOTH list in either tenant, and for the CA list only when `$tenantKey === 'CA'`. Two consumers:
75
78
  1. **The inactivation loop** in `executeForTenant()` skips any user whose `Users.email` is protected for that tenant, so the batch `UPDATE ... SET isActive = 0` never lists them.
76
79
  2. **`ensureCrossRegionUsersActive()`**, which runs after both tenants execute and force-sets `isActive = 1` — in both databases for the BOTH list, and in `Client_CompassCanada` only for the CA list.
77
80
 
78
- **Today only layer 2 actually protects the CA-only pair.** In `Client_CompassCanada` both of those users have `c_hrEmpUsername = NULL`, and `loadLookups()` keys its map on username — so they never enter the map and can never reach the inactivation batch on the Canada side. The loop guard is the defence for the day HR gives them a Canada username. (Read-only prod check, 2026-09-16.)
81
+ **For the `@compassdigital.io` pair, only layer 2 is live.** In `Client_CompassCanada` both of those users have `c_hrEmpUsername = NULL`, and `loadLookups()` keys its map on username — so they never enter the map and can never reach the inactivation batch on the Canada side. (Read-only prod check, 2026-09-16.) **James Fuller is the opposite case**: his Canada row carries `c_hrEmpUsername = 'fullej01'`, so he *does* enter the map and **layer 1 is what actually saves him** — the inactivation loop guard is not theoretical.
79
82
 
80
83
  **The email match works across a case difference.** `Client_Compass` stores these two addresses UPPERCASE while `Client_CompassCanada` stores them lowercase. `Users.email` uses `utf8mb4_0900_ai_ci`, a case-insensitive collation, so `WHERE email = '...'` still hits — each address resolves to exactly **one** row per database.
81
84
 
@@ -105,8 +108,8 @@ The insert-time hook designed 2026-08-12 was built and grew into a reusable engi
105
108
  - **⚠ The `Users` ↔ `Contacts` email/phone mirror is reached through the PRIMARY pointers, not through `name = 'WORK'`.** `PeopleFile.php:300-310` joins on `Contacts.primaryContactEmailAddressId` / `Contacts.primaryContactPhoneNumberId`. `name = 'WORK'` is only set at **insert**, and there is **no unique key on `(contactId, name)`** — so a contact can hold several `WORK` rows and "the WORK row" is **not deterministic**. Follow the primary pointers; never match on the name.
106
109
  - **⚠ The importer re-syncs the `Contacts` child rows UNCONDITIONALLY for every existing user.** `PeopleFile.php:630-632` runs **outside** the `hasDiff` gate, so a `Contacts` email/phone edit made by hand or by a tool is reverted on the next run **even when the `Users` row itself does not diff**. Any out-of-band contact edit is temporary in exactly the same way an out-of-band activation is.
107
110
  - **⚠ `executeForTenant()`'s inactivation sweep is the ONLY place in the file that writes `isActive = 0`.** Every other `isActive` path in the ~2,200-line class only forces it back to **1**. Two consequences: (a) **an activation made outside this cron is temporary** — the next run deactivates the person again unless they are in that day's PEOPLE file — while (b) **a deactivation is durable**, because nothing here ever reactivates anyone on its own. This makes the [Tools Compass admin pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md) a one-way-durable tool.
108
- - **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no `isActive` filter**, so it reactivates **every** row carrying a protected address, dead duplicates included. Verified prod 2026-09-02 for the original four, and 2026-09-16 for the two CA-only addresses: each resolves to exactly **one** row per database, so nothing is being resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list is also the **only** durable way to hold a cross-region user active, and changing it needs a code change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list instead of matching on email** — matching on email means an email edit silently moves a user in or out of the protected set, which is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks the protected addresses from being edited at all.
109
- - **⚠ The Tools admin's edit block covers only the original four addresses, not the two CA-only ones.** The protected set grew to six in worker2 on 2026-09-16; the `tools` block list was not changed and was not re-checked. Until it is, someone can edit the email of a CA-protected user in [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) and silently drop them out of the protected set — because the match is on email, not id.
111
+ - **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no `isActive` filter**, so it reactivates **every** row carrying a protected address, dead duplicates included. Verified prod 2026-09-02 for the original four, and 2026-09-16 for the first two CA-only addresses: each resolves to exactly **one** row per database, so nothing is being resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list is also the **only** durable way to hold a cross-region user active, and changing it needs a code change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list instead of matching on email** — matching on email means an email edit silently moves a user in or out of the protected set, which is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks the protected addresses from being edited at all.
112
+ - **⚠ The Tools admin's edit block covers only the original four addresses, not the CA-only ones.** The protected set grew to six in worker2 on 2026-09-16 and to **seven** on 2026-09-28; the `tools` block list was not changed and was not re-checked. Until it is, someone can edit the email of a CA-protected user in [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) and silently drop them out of the protected set — because the match is on email, not id.
110
113
  - **⚠ Adding a new "always keep this person active" rule means touching exactly two places.** `executeForTenant()`'s inactivation loop is the only `isActive = 0` writer, and `ensureCrossRegionUsersActive()` is the post-run sweep. Guard both and the requirement is covered; guard only the loop and the post-run sweep still decides the final state; guard only the sweep and the user flickers inactive between the two steps.
111
114
 
112
115
  - **⚠ A whole tenant run is ONE transaction, and US runs first — so a US exception means Canada never executes at all.** Any throw rolls back everything for that run: inserts, updates, reactivations, **deactivations**, personas, roles and approval reassignment. The 2026-09-22 FK failure blocked both tenants for a day this way. Rollback is clean (proven: a run that died on `Users_Personas` after the `Users` insert left 0 rows behind), and the exception aborts **before** the SFTP delete, so the input file survives and the next run reprocesses it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.902",
3
+ "version": "1.0.903",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",