toga-ai 1.0.657 → 1.0.658

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
+ | [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
6
7
  | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql |
7
8
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
8
9
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-26
10
+ owners: [bala]
11
+ files:
12
+ - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
+ related:
14
+ - ../architecture.md
15
+ - ../../_underscore/features/acl-permission-chain.md
16
+ - ../../_underscore/features/tracking-number-bridges.md
17
+ - ../../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ Most `dbchanges2` files are **additive data grants** run by hand against production, often more than
23
+ once (once per environment, or twice because someone was not sure it took). Two mechanics decide
24
+ whether that is safe: how you generate the `uuid` every 2.0 table demands, and how you guard the
25
+ insert. Both have a trap that fails **silently** rather than erroring.
26
+
27
+ Every 2.0 table carries a `uuid` column that is `char(36) NOT NULL` **with no default** — the model
28
+ layer fills it in application code, so a hand-written `INSERT` must supply it itself.
29
+
30
+ ## How it works
31
+
32
+ ### Generating a real UUID4 inline
33
+
34
+ The standard is "UUID4, random — never MySQL `UUID()`" (`UUID()` is v1: MAC address + timestamp, and
35
+ sequential). MySQL 8's `RANDOM_BYTES()` gives a genuinely random, correctly **version-4-shaped** value
36
+ in one expression:
37
+
38
+ ```sql
39
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), '-', HEX(RANDOM_BYTES(2)), '-4', RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', SUBSTRING('89ab', FLOOR(1 + RAND() * 4), 1), RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', HEX(RANDOM_BYTES(6))))
40
+ ```
41
+
42
+ The two literals are what make it v4 rather than just random hex: the `'-4'` pins the version nibble,
43
+ and `SUBSTRING('89ab', …)` picks the variant nibble. Verified working on prod MySQL **8.0.39**.
44
+
45
+ **Existing rows are not the pattern to copy.** Plenty of live rows across these tables are random hex
46
+ that is *not* v4-shaped (older migrations concatenated `RANDOM_BYTES` without the version/variant
47
+ nibbles). They are fine and must not be "fixed" — but do not use them as the template for new SQL.
48
+
49
+ ### ⚠ `DISTINCT` cannot dedupe a row that carries a random uuid
50
+
51
+ This is the silent one. In an `INSERT … SELECT`, putting the uuid expression inside a
52
+ `SELECT DISTINCT` **disables the dedupe entirely** — the per-row random uuid makes every candidate
53
+ row unique, so `DISTINCT` has nothing to collapse and every duplicate id passes through.
54
+
55
+ ```sql
56
+ # WRONG — DISTINCT never removes anything; the uuid differs on every row
57
+ INSERT INTO Personas_Items (uuid, personaId, itemId)
58
+ SELECT DISTINCT
59
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
60
+ @personaId,
61
+ bi.itemId
62
+ FROM BundleItems bi
63
+ INNER JOIN Bundles b ON b.id = bi.bundleId
64
+ WHERE b.number = '243';
65
+
66
+ # CORRECT — dedupe the id set in a derived table, generate the uuid in the OUTER select
67
+ INSERT INTO Personas_Items (uuid, personaId, itemId)
68
+ SELECT
69
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
70
+ @personaId,
71
+ src.itemId
72
+ FROM (
73
+ SELECT DISTINCT bi.itemId
74
+ FROM BundleItems bi
75
+ INNER JOIN Bundles b ON b.id = bi.bundleId
76
+ WHERE
77
+ b.number = '243' AND
78
+ bi.isActive = 1 AND
79
+ bi.itemId IS NOT NULL
80
+ ) src;
81
+ ```
82
+
83
+ The same shape is the fix for the self-referencing `INSERT … NOT EXISTS` error 1093 (MySQL will not
84
+ let you read the table you are inserting into) — wrap the read in a derived table.
85
+
86
+ ### Guarding the insert: know whether the table has a unique key
87
+
88
+ Whether a re-run is loud or silent depends entirely on the target table's indexes, and bridge tables
89
+ in 2.0 are **inconsistent** about this — some have a composite unique key, some do not. Check
90
+ before you write, not after.
91
+
92
+ - **Unique key present** — a re-run errors. Loud, recoverable, but it aborts the rest of the file.
93
+ Add a guard if you want a clean no-op.
94
+ - **No unique key** — a re-run **silently duplicates every row**, and nothing tells you. Always guard
95
+ these.
96
+
97
+ Both guard shapes are in use and equivalent:
98
+
99
+ ```sql
100
+ # LEFT JOIN … IS NULL
101
+ FROM Users u
102
+ INNER JOIN Roles r ON r.name = 'SuperUser'
103
+ LEFT JOIN Users_Roles ur ON ur.userId = u.id AND ur.roleId = r.id
104
+ WHERE
105
+ u.email = '…' AND
106
+ ur.id IS NULL;
107
+ ```
108
+
109
+ **A guarded insert that inserts zero rows is a success, not a wasted file** — see
110
+ [Client-DB grant migrations are re-runnable NO-OPs](../../_underscore/features/acl-permission-chain.md).
111
+
112
+ ### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
113
+
114
+ The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
115
+ against `@parentId` — means **every phase must run in the same connection**. Reconnecting between
116
+ phases leaves `@parentId` `NULL` and the child inserts either fail on the FK or write `NULL`. Say so
117
+ in the file header, and make the unguarded parent insert the one phase you skip on a re-run (set
118
+ `@parentId` to the existing row by hand instead).
119
+
120
+ ## Gotchas
121
+
122
+ - **A "re-run safe" file is usually only re-run safe *per phase*.** If Phase 1 creates the parent row
123
+ with no `NOT EXISTS` guard, re-running the *whole file* creates a **second parent** and then hangs
124
+ a full set of children off it. Re-run safety of the child phases does not make the file idempotent.
125
+ - **`number` columns are strings.** `Bundles.number`, `Personas.number` and friends are varchar, not
126
+ int — quote them (`WHERE b.number = '243'`) and `CAST(… AS UNSIGNED)` when you need numeric sort
127
+ order. An unquoted comparison forces a string→number coercion that will not use the index.
128
+ - **`Client_*` files may not reach into `Core`.** Unrelated to uuids but it bites the same kind of
129
+ file: separate production clusters mean a `Core.` reference is unrunnable in prod even though it
130
+ works locally. Hardcode `Core.Records` / `Core.RecordFields` ids as literals — see
131
+ [dbchanges2 architecture](../architecture.md).
132
+
133
+ ## Change history
134
+ - 2026-08-26 — Initial: recorded the inline **UUID4** expression built on MySQL 8 `RANDOM_BYTES()`
135
+ (version/variant nibbles pinned; verified on prod 8.0.39) and the silent
136
+ **`SELECT DISTINCT` + random-uuid** trap that lets duplicate rows through — dedupe the id set in a
137
+ derived table and generate the uuid in the outer select. Added the unique-key/guard rule for bridge
138
+ tables (no unique key = silent duplicates on re-run) and the `LAST_INSERT_ID()` one-session
139
+ constraint. Extracted from the Compass Creative Studio persona grant. (bala)
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 67 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
- - **dbchanges2** (Database Changes) _(framework core)_ — 9 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
+ - **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
@@ -20,6 +20,7 @@
20
20
  | [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
21
21
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
22
22
  | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
23
+ | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
23
24
  | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
24
25
  | [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
25
26
  | [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json, library/app/client/compass.php |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-13
9
+ updated: 2026-08-26
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker2/Worker/Client/Compass/PeopleFile.php
@@ -15,6 +15,7 @@ related:
15
15
  - ../profile.md
16
16
  - ../workflows/persona-refactor-migration.md
17
17
  - ../workflows/persona-population-env-comparison.md
18
+ - ../workflows/granting-persona-bundle-access.md
18
19
  - ../../compass-canada/profile.md
19
20
  ---
20
21
 
@@ -40,16 +41,25 @@ below.
40
41
 
41
42
  | Persona | id | Meaning |
42
43
  |---|---|---|
43
- | Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. |
44
+ | Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. **Still named "All" in prod** — the refactor is not live there (verified 2026-08-26). |
44
45
  | Levy | **24** | Levy-sector catalogue. |
45
- | Non-Levy | **resolved by name** — 38 on dev-sandbox *and* prod | Everything that is not a printer. Granted to non-Levy users only. |
46
+ | Non-Levy | **resolved by name** — **38 on dev-sandbox only; does not exist in prod** | Everything that is not a printer. Granted to non-Levy users only. |
46
47
  | VIP | 35 | VIP add-on. |
47
48
  | CDL (MacBook / Surface) | 30 | `@compassdigital.io` + VIP users. |
48
49
 
49
- **Never hardcode the Non-Levy id.** `Personas.AUTO_INCREMENT` is 38 on both dev-sandbox and prod
50
- (max existing id 37), so the migration's `number = '40'` yields **id 38**, not 40. The original
51
- plan and the (now closed) worker2 PR both hardcoded **40** and would have written to the wrong
52
- persona. `PeopleFile` resolves it at runtime:
50
+ **Never hardcode the Non-Levy id.** On dev-sandbox `Personas.AUTO_INCREMENT` was 38 (max existing id
51
+ 37), so the migration's `number = '40'` yielded **id 38**, not 40. The original plan and the (now
52
+ closed) worker2 PR both hardcoded **40** and would have written to the wrong persona.
53
+
54
+ > **⚠ Correction (verified read-only against prod, 2026-08-26): "id 38" was never a prod fact, and is
55
+ > now definitively wrong there.** Prod has **no Non-Levy persona**, and `Personas.id` **38** is
56
+ > **"MyDining- KDS"** — prod's auto-increment moved past 38 after 2026-08-06. Persona **number 40** is
57
+ > taken by that same row. Whenever the migration does run in prod the persona will land on some higher
58
+ > id, which is precisely what name-resolution protects against. Current prod number map and the
59
+ > knock-on effect on the migration's Phase 1:
60
+ > [Persona Refactor](../workflows/persona-refactor-migration.md).
61
+
62
+ `PeopleFile` resolves it at runtime:
53
63
 
54
64
  ```php
55
65
  const PERSONA_NAME_US_NON_LEVY = 'Non-Levy';
@@ -128,6 +138,14 @@ VIP (35) and CDL (30) handling is orthogonal and applies to both tenants unchang
128
138
  constants from this file into tickets, docs, or chat transcripts.
129
139
 
130
140
  ## Change history
141
+ - 2026-08-26 — **Correction from a read-only prod check.** The "Non-Levy = id **38** on dev-sandbox
142
+ *and* prod" claim is wrong: prod has **no Non-Levy persona** and id 38 is **"MyDining- KDS"**, so
143
+ prod's auto-increment is past 38 and persona 1 is still named **"All"**. Reinforces resolve-by-name.
144
+ Also linked the new [bundle-grant workflow](../workflows/granting-persona-bundle-access.md), which
145
+ covers the *other* half of the persona model this doc does not: what a persona grant physically
146
+ consists of (`Personas_Bundles` + **`Personas_Items`** + `Personas_Assortments`) and that the
147
+ effective persona set is a UNION of the user's own personas, their location's, and their sector's.
148
+ (bala)
131
149
  - 2026-08-13 — Split the `Users` row lifecycle out of this doc into
132
150
  [PEOPLE-file User Lifecycle](./people-file-user-lifecycle.md) (duplicate accounts from the 5-day
133
151
  lookup window; raw-SQL deactivation that fires no model hook). This doc stays scoped to persona /
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: compass-usa
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-08-25
21
+ updated: 2026-08-26
22
22
  owners: [jcardinal, bala, tcox, apeterson, dfranks]
23
23
  files: []
24
24
  related:
@@ -27,6 +27,7 @@ related:
27
27
  - features/people-file-user-lifecycle.md
28
28
  - workflows/persona-refactor-migration.md
29
29
  - workflows/persona-population-env-comparison.md
30
+ - workflows/granting-persona-bundle-access.md
30
31
  - features/mits-sales-order-transmission-alerting.md
31
32
  - features/asn-to-item-fulfillment.md
32
33
  - features/cost-centers.md
@@ -132,8 +133,17 @@ separate, related client (see its own profile).
132
133
  users got the full catalogue). Never trust pre-2026-08 Levy persona state as intentional.
133
134
  - [Persona Refactor — 8-phase migration & deploy order](workflows/persona-refactor-migration.md)
134
135
  (TRUE-75705) — splits persona 1 into Base (printers, everyone) + Non-Levy. **Run on dev-sandbox
135
- only; production is NOT migrated.** worker2 must deploy first; Phase 8 (ACL expression 235) ships
136
- with the toga2-commerce hack removal.
136
+ only; production is NOT migrated** (re-verified 2026-08-26: prod persona 1 is still "All", there is
137
+ no Non-Levy persona, and id 38 is now "MyDining- KDS"). worker2 must deploy first; Phase 8 (ACL
138
+ expression 235) ships with the toga2-commerce hack removal.
139
+ - [Granting a user access to a bundle (persona grant) + SuperUser](workflows/granting-persona-bundle-access.md)
140
+ — the recurring "give user X sight of kit N" request. **Five tables, not one**: a bundle grant
141
+ without `Personas_Items` renders a **half-visible kit**, because item-level ACL hides kit lines
142
+ whose item the persona cannot see. Also: a Compass persona needs **no** settings rows (all nine
143
+ persona-settings tables are empty across all 38 prod personas), "bundle 243" is `Bundles.number`
144
+ not `id`, and **`Users` has no `username` column** — it is `c_hrEmpUsername`, stored lowercase.
145
+ Super user = `Roles.id 5` added **additively** via `Users_Roles`, never replacing Base /
146
+ Compass Base.
137
147
  - [Prod ↔ dev-sandbox persona comparison](workflows/persona-population-env-comparison.md) — the
138
148
  full-population diff technique, the **join-on-email** rule (`Users.id` means different people per
139
149
  environment), and the drift bundles (188/191/242) that mimic a catalogue grant.
@@ -0,0 +1,163 @@
1
+ ---
2
+ title: Granting a Compass user access to a bundle (persona grant) and the SuperUser role
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-26
10
+ owners: [bala]
11
+ files:
12
+ - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
+ related:
14
+ - ../features/persona-model-and-levy-gating.md
15
+ - ./persona-refactor-migration.md
16
+ - ./persona-population-env-comparison.md
17
+ - ../profile.md
18
+ - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
19
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a
25
+ super user". It looks like a one-row insert into `Personas_Bundles`. It is not — a bundle grant
26
+ without the matching **item** grant renders a **half-visible kit**, because item-level ACL hides any
27
+ kit line whose item the persona cannot see.
28
+
29
+ A complete grant touches **five tables**. `dbchanges2/Client_Compass/2026-08-26a -
30
+ CompassCreativeStudioPersona.sql` is the working template: a 6-phase additive script that creates the
31
+ persona "Compass Creative Studio" (number 41), grants bundle 243, its 19 items and the 6 assortments
32
+ those items live in, assigns the persona to one user, and adds SuperUser.
33
+
34
+ All facts below were verified read-only against **production `Client_Compass` on 2026-08-26**.
35
+
36
+ ## The five tables
37
+
38
+ | Table | Grants | Composite unique key? | Optional? |
39
+ |---|---|---|---|
40
+ | `Personas` (`id`, `uuid`, `name`, `number`) | the persona itself | — | no |
41
+ | `Personas_Bundles` (`personaId`, `bundleId`) | the kit appears | **yes** — re-run **errors** | no |
42
+ | `Personas_Items` (`personaId`, `itemId`) | the kit's **lines** are visible | **no** — re-run **silently duplicates** | **no — see below** |
43
+ | `Personas_Assortments` (`personaId`, `assortmentId`) | browsing the accessory categories | **no** — re-run **silently duplicates** | no |
44
+ | `Users_Personas` (`userId`, `personaId`) | the user holds the persona | **yes** — re-run **errors** | no |
45
+
46
+ That asymmetry is the thing to remember: the two bridges that most need a guard are exactly the two
47
+ that will not protect themselves.
48
+
49
+ ### `Personas_Items` is not optional
50
+
51
+ Every `BundleItems.itemId` of the bundle must also be granted to the persona. Concretely: of bundle
52
+ 243's **19** active line items, persona 1 ("All", which the user already held) covered only **2** —
53
+ so a bundle-only grant would have shown the kit with 17 of its lines missing.
54
+
55
+ ### Derive the assortment set — never hardcode it
56
+
57
+ An assortment qualifies when it holds one of the bundle's items **and** the persona that owns the
58
+ bundle today already grants it. Joining it out means the new persona inherits exactly the browse
59
+ scope the current owner has, with no list to maintain:
60
+
61
+ ```sql
62
+ SELECT DISTINCT ai.assortmentId
63
+ FROM Bundles b
64
+ INNER JOIN BundleItems bi ON bi.bundleId = b.id AND bi.isActive = 1
65
+ INNER JOIN AssortmentItems ai ON ai.itemId = bi.itemId
66
+ INNER JOIN Personas_Bundles pbSource ON pbSource.bundleId = b.id AND pbSource.personaId <> @personaId
67
+ INNER JOIN Personas_Assortments pa ON pa.assortmentId = ai.assortmentId AND pa.personaId = pbSource.personaId
68
+ WHERE b.number = '243'
69
+ ```
70
+
71
+ The `personaId <> @personaId` exclusion matters: by the time this phase runs, the **new** persona
72
+ also owns the bundle, and without it the join would feed on itself.
73
+
74
+ For bundle 243 this resolves to assortments **2** (Monitors), **4** (Computer Accessories), **6**
75
+ (Apple Accessories), **8** (Keyboard/Keypad & Mouse), **10** (Cables) and **19** (Computers).
76
+ **An inactive assortment in the set is normal** — 19 is `isActive = 0` and is still granted, because
77
+ the grant mirrors the source persona rather than judging the data.
78
+
79
+ ## A new Compass persona needs NO settings rows
80
+
81
+ Verified across **all 38** personas in prod: `PersonaAppSettings`, `PersonaGlobalSettings`,
82
+ `PersonaPageSettings`, `PersonaRecordSettings`, `PersonaSectionSettings`,
83
+ `PersonaRecordFieldSettings`, `PersonaTranslations`, `Personas_Currencies` and
84
+ `Personas_VendorItems` are **all empty — 0 rows**. A Compass persona is purely **catalogue scoping**
85
+ (bundles / items / assortments). Do not go hunting for supporting configuration; there is none.
86
+
87
+ ## Persona numbers in prod (2026-08-26)
88
+
89
+ **38 personas.** Numbers in use: **1-23, 27-40, and 100** ("QA Full Access", id 37). **The next free
90
+ number is 41.** `Personas.number` is a *string* column, so quote it.
91
+
92
+ ## "Bundle 243" is `Bundles.number`, not `Bundles.id`
93
+
94
+ The business always quotes the **number**. Bundle **243** is `Bundles.id` **171** ("Compass Creative
95
+ Studio MacBook"). Resolve it in the query (`WHERE b.number = '243'`) rather than pasting an id, and
96
+ never assume the two match.
97
+
98
+ ## Finding the user
99
+
100
+ **`Client_Compass.Users` has no `username` column** — the natural first query fails with
101
+ `Unknown column 'username'`. The Compass login / personnel username lives in **`c_hrEmpUsername`**,
102
+ and it is stored **lowercase**: people quote it uppercase ("KINGK01") but the row says `kingk01`.
103
+ Look users up by `email` or `c_hrEmpUsername` (lowercased), which is also how the nightly PEOPLE cron
104
+ matches them — see [PEOPLE-File User Lifecycle](../features/people-file-user-lifecycle.md).
105
+
106
+ ## Making someone a super user
107
+
108
+ There is **no `isSuperUser` flag on `Users`** — it is a role row. `Client_Compass.Roles.id` **5**,
109
+ name **`SuperUser`** (113 holders as of 2026-08-26), granted **additively** through
110
+ `Users_Roles (userId, roleId)`, which is unique on the pair.
111
+
112
+ **SuperUser is never a replacement for the base roles.** The established prod pattern for a Compass
113
+ super user is **Base (1) + SuperUser (5) + Compass Base (7)**, optionally **+ Manager (8)**. Role
114
+ permissions resolve as a **union** (see
115
+ [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)), so adding
116
+ role 5 while removing 1 or 7 takes access *away*.
117
+
118
+ ## Verify the way the ACL actually resolves personas
119
+
120
+ A user's **effective** persona set is three UNIONed sources, not just `Users_Personas`:
121
+
122
+ 1. `Users_Personas` — personas held directly.
123
+ 2. `Locations_Personas` for the user's own `locationId`.
124
+ 3. `Locations_Personas` on the user's **sector** — five `parentLocationId` hops up
125
+ (location → complex → district → region → division → sector).
126
+
127
+ **Check all three before adding anything** — the user may already reach the bundle through their
128
+ location, in which case the right answer is "no change needed". After the grant, re-run the same
129
+ resolution and confirm the bundle number appears; the verification block at the end of the template
130
+ file does exactly this. The same three-source resolution is what the full-population comparison
131
+ tooling uses — see
132
+ [Prod vs dev-sandbox persona comparison](./persona-population-env-comparison.md).
133
+
134
+ ## Gotchas
135
+
136
+ - **⚠ Re-running the whole file creates a SECOND persona.** Phase 1's `INSERT INTO Personas` has no
137
+ guard, so a second run inserts another persona with the same name and number, and the child phases
138
+ then hang a full set of rows off it. The child phases are individually re-run safe; the **file is
139
+ not idempotent**. To repeat any phase, set `@personaId` to the existing persona id by hand and skip
140
+ Phase 1.
141
+ - **`@personaId` is a session variable — run every phase in ONE session.** A reconnect between phases
142
+ leaves it `NULL`.
143
+ - **`SELECT DISTINCT` will not dedupe a row carrying a generated uuid.** The per-row random value
144
+ makes every row distinct. Dedupe the id set in a derived table and generate the uuid in the outer
145
+ select — full explanation and the UUID4 expression in
146
+ [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
147
+ - **⚠ Do not write this SQL against the persona-refactor target state.** As of 2026-08-26 the refactor
148
+ has **not** shipped to prod: persona 1 is still named **"All"** (not "Base"), there is **no
149
+ "Non-Levy" persona**, and persona **number 40 belongs to id 38 "MyDining- KDS"**. See
150
+ [Persona Refactor](./persona-refactor-migration.md).
151
+ - **Everything here is additive.** The user keeps persona 1 and their existing roles; nothing is
152
+ removed. A "remove access" request is a different, destructive job and needs its own review.
153
+
154
+ ## Change history
155
+ - 2026-08-26 — Initial: documented the five-table bundle grant (and that **`Personas_Items` is
156
+ mandatory** — item-level ACL hides kit lines whose item the persona cannot see; persona 1 covered
157
+ only 2 of bundle 243's 19 items), the unique-key asymmetry that makes `Personas_Items` /
158
+ `Personas_Assortments` duplicate silently on re-run, the **derived** assortment join, and that
159
+ **every persona settings table in prod is empty across all 38 personas** so a new persona needs no
160
+ config. Added the `Bundles.number` vs `Bundles.id` trap, the missing `Users.username` column
161
+ (`c_hrEmpUsername`, lowercase), the SuperUser role recipe (`Roles.id 5`, additive, Base + SuperUser
162
+ + Compass Base), and the three-source effective-persona verification. Template file:
163
+ `2026-08-26a - CompassCreativeStudioPersona.sql` (written, not executed). (bala)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-26
10
10
  owners: [bala]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-06 - PersonaRefactor.sql
@@ -16,6 +16,7 @@ files:
16
16
  related:
17
17
  - ../features/persona-model-and-levy-gating.md
18
18
  - ./persona-population-env-comparison.md
19
+ - ./granting-persona-bundle-access.md
19
20
  - ../profile.md
20
21
  ---
21
22
 
@@ -29,6 +30,32 @@ Levy-sector users). One SQL file, **8 phases**, plus a companion verification sc
29
30
  > (toga2-commerce #306, dbchanges2 #368, worker2 #43) were closed and the work rewritten against
30
31
  > current code and data — treat any plan or skill text predating 2026-08-06 as stale.
31
32
 
33
+ ### ⚠ Everything below Phase 1 describes a TARGET state. Prod is not in it (re-verified 2026-08-26)
34
+
35
+ The phase table, the persona ids and the post-migration figures in this doc describe what prod will
36
+ look like **after** the migration. Read read-only against production `Client_Compass` on
37
+ **2026-08-26**, prod is still pre-migration:
38
+
39
+ | Claim you might carry over | Production reality, 2026-08-26 |
40
+ |---|---|
41
+ | Persona 1 is named "Base" | Persona 1 is still named **"All"** |
42
+ | A "Non-Levy" persona exists | **It does not exist in prod at all** |
43
+ | Non-Levy will land on `Personas.id` **38** | **Id 38 is already taken** — it is **"MyDining- KDS"** |
44
+ | Phase 1's hardcoded `number = '40'` is free | **Number 40 is in use** (by id 38). Numbers **1-23, 27-40 and 100** are taken across **38** personas; **41 is the next free number** |
45
+
46
+ Two consequences before this is ever run in prod:
47
+
48
+ 1. **Phase 1's hardcoded `number = '40'` must be re-checked** — it would create a second persona
49
+ carrying a number that already belongs to "MyDining- KDS".
50
+ 2. **The "id will be 38" reasoning no longer holds in prod.** `Personas.AUTO_INCREMENT` has moved
51
+ past 38 since 2026-08-06, so the new persona will get some higher id. This is exactly why
52
+ `PeopleFile` resolves Non-Levy **by name** — see
53
+ [Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Never hardcode
54
+ the id, and re-read prod rather than this table before running.
55
+
56
+ **Any other Compass persona work must target prod-as-it-is, not this target state** — see
57
+ [Granting a Compass user access to a bundle](./granting-persona-bundle-access.md).
58
+
32
59
  The persona semantics and the Levy-detection code this depends on live in
33
60
  [Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Read that first;
34
61
  the migration is meaningless without the `isLevySector` fix.
@@ -130,6 +157,12 @@ All checks are written as **invariants** or as comparisons against a captured **
130
157
  | Fixed verification user list (HAQIKAH.AARON, KYLE.AARON) | HAQIKAH.AARON is inactive, KYLE.AARON does not exist — **find test users dynamically** |
131
158
 
132
159
  ## Change history
160
+ - 2026-08-26 — **Correction, not new work.** Re-verified read-only against prod: the refactor is
161
+ **still not live**. Prod persona 1 is named **"All"** (not "Base"), there is **no Non-Levy
162
+ persona**, and **id 38 is now "MyDining- KDS"** — so the "Non-Levy = id 38" reasoning and Phase 1's
163
+ hardcoded `number = '40'` (number 40 belongs to id 38) are both stale for prod. Prod holds 38
164
+ personas using numbers 1-23, 27-40 and 100; next free number is 41. Added the target-state warning
165
+ block so nobody writes prod SQL against the post-migration table. (bala)
133
166
  - 2026-08-06 — Initial: rewritten 8-phase migration (earlier draft's Phase 5+9 collapsed into one
134
167
  Levy-excluding assignment; Phase 6 restricted to active users; Phase 7f deletes the retired persona
135
168
  by name; Phase 8 makes ACL expression 235 purely additive). Documented the B2 verification-order
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.657",
3
+ "version": "1.0.658",
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",