toga-ai 1.0.498 → 1.0.500
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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -1
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +79 -5
- package/knowledge/1.0/apps/test/INDEX.md +1 -0
- package/knowledge/1.0/apps/test/features/static-no-db-regression-harness.md +100 -0
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +23 -3
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/architecture.md +46 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md +113 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/aig/features/entitlement-intake.md +40 -15
- package/knowledge/sessions/2026-08-03-true-79401-multi-email-resolution-mhammontree.md +64 -41
- package/package.json +1 -1
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
15
15
|
| [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | `App_SystemMonitor_NetSuiteIntegration` (`library/app/systemmonitor/netsuiteintegration.php`, title **"NetSuite Sync Alert"**) is a 1.0 system monitor that watc | library/app/systemmonitor/netsuiteintegration.php, worker/crons/infrastructure/system_monitors.php |
|
|
16
16
|
| [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php, library/app/api/startechticket.php, worker/crons/toga2/startech/common_import_supporting_records.php |
|
|
17
|
-
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php |
|
|
17
|
+
| [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` (`library/app/api/toga2.php`, ~8400 lines) is the **1.0-side client for the TOGa 2 (`_underscore`/api2) public API** *and* the home of the cross | library/app/api/toga2.php, worker/crons/toga2/aig/sync_togasupply_aig.php, worker/crons/toga2/wje/sync_togasupply_wje.php, test/@Mark/AIG/test_multi_email.php |
|
|
@@ -12,9 +12,12 @@ files:
|
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- worker/crons/toga2/aig/sync_togasupply_aig.php
|
|
14
14
|
- worker/crons/toga2/wje/sync_togasupply_wje.php
|
|
15
|
+
- test/@Mark/AIG/test_multi_email.php
|
|
15
16
|
related:
|
|
16
17
|
- ../../../2.0/apps/api2/features/nested-relationship-writes.md
|
|
17
18
|
- ../../../clients/aig/features/entitlement-intake.md
|
|
19
|
+
- ../../test/features/static-no-db-regression-harness.md
|
|
20
|
+
- ../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md
|
|
18
21
|
- netsuite-suiteql-api-reference.md
|
|
19
22
|
- netsuite-suiteql-rest-shim.md
|
|
20
23
|
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
@@ -138,7 +141,28 @@ Correct inbound shape:
|
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
The primary **may** be repeated inside the collection — `syncContactToToga1Customer` de-dupes with
|
|
141
|
-
`array_unique
|
|
144
|
+
`array_unique` — and AIG **should** repeat it: if the collection carries extras but **omits** the
|
|
145
|
+
primary, the primary is **not** re-added (the fallback fires only on a *fully empty* list), so the
|
|
146
|
+
1.0 field ends up holding the secondary/tertiary **without** the main address.
|
|
147
|
+
|
|
148
|
+
**A wrong shape degrades silently to primary-only — it does NOT blank the field (corrected
|
|
149
|
+
2026-08-03, proven by the static harness).** With an array of bare strings, `array_column()` finds
|
|
150
|
+
no `emailAddress` key → empty list → **the primary-email fallback fires** → the stored value is
|
|
151
|
+
exactly the primary address, **byte-identical to correct single-email behavior.** So there is **no
|
|
152
|
+
observable difference between "AIG sent the wrong shape" and "AIG hasn't enabled the feature yet"**
|
|
153
|
+
— strictly worse for diagnosis than a blanked field. The **only** reliable verification signal is
|
|
154
|
+
the child row count in `Client_Aig.ContactEmailAddresses`: not the HTTP status, and **not** the 1.0
|
|
155
|
+
`Customers.emailAddress` value.
|
|
156
|
+
|
|
157
|
+
**Storage is bounded — "send as many as you like" is FALSE at the 1.0 layer.** With `'; '` joining
|
|
158
|
+
and ~47-char business addresses, measured lengths against the `varchar(255)` column: 2 addr = 96,
|
|
159
|
+
3 = 145, 5 = 243 chars (all fit); **8 = 390 and 10 = 489 chars → OVERFLOW.** So the practical cap
|
|
160
|
+
is roughly **6–7 business-length addresses.** The committed scope (secondary + tertiary ⇒ 3
|
|
161
|
+
addresses ≈ 145 chars) is comfortably safe, but the 2.0 nested collection is structurally
|
|
162
|
+
**unbounded** while the 1.0 column is not — the "1, 2, 3 or 10 all work with no change on our side"
|
|
163
|
+
pitch is true of 2.0 storage only, and must be qualified for 1.0. Overflow behavior is itself
|
|
164
|
+
machine-dependent (silent truncation under an empty `sql_mode`, `ERROR 1406` under strict mode) —
|
|
165
|
+
see [local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
142
166
|
|
|
143
167
|
**Three distinct layers — only the last one is delimited.** Conflating them is the easy mistake:
|
|
144
168
|
|
|
@@ -164,8 +188,18 @@ tickets) was **explicitly rejected** — see the gotcha about the single-recipie
|
|
|
164
188
|
|
|
165
189
|
### Open items (TRUE-79401)
|
|
166
190
|
|
|
167
|
-
1.
|
|
168
|
-
|
|
191
|
+
1. **Merged: YES. Deployed: still unverified.** Verified 2026-08-03: branch `TRUE-79401`
|
|
192
|
+
(`5aca31ea`) is **fully merged up with `_production`** (`_production` is an ancestor of HEAD,
|
|
193
|
+
working tree clean, local `_production` == `origin/_production` as of the last fetch — no network
|
|
194
|
+
fetch was run). The ticket's own patch is **still +17 / −2 in `app/api/toga2.php`** against
|
|
195
|
+
`_production`: the two merge commits (`90b4178b`, `5aca31ea`) pulled in ~404 insertions of
|
|
196
|
+
**other** tickets' work (TRUE-80408/80374, plus the error-capture and ServiceNow-OAuth commits)
|
|
197
|
+
and **none** of them touched `syncContactToToga1Customer`, the `/contacts` fetch `fields` list, or
|
|
198
|
+
any TRUE-79401 identifier. `php -l` clean on PHP 7.2.33; harness drift guard OK; 12/12 cases pass
|
|
199
|
+
after the merges. **A reviewer should not be alarmed by the file's churn** — the small patch is
|
|
200
|
+
intact under a large merge. **Still open: whether it is deployed to the prod worker.**
|
|
201
|
+
Review technique worth reusing: `git diff _production...HEAD -- <file>` isolates a ticket's real
|
|
202
|
+
contribution from merge noise when an old branch is finally synced.
|
|
169
203
|
2. **Beta verification of the nested write is outstanding.** Assert real
|
|
170
204
|
`Client_Aig.ContactEmailAddresses` **row counts** (never the HTTP status), covering **both** a
|
|
171
205
|
brand-new contact (CREATE) **and** a second entitlement for an **existing** contact (UPDATE).
|
|
@@ -250,7 +284,9 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
250
284
|
framework-level fact captured separately.)
|
|
251
285
|
- **`TOGA_AIG.Customers.emailAddress` is `varchar(255)`** (V1 legacy, latin1_swedish_ci) — wide
|
|
252
286
|
enough to hold several `'; '`-joined emails, so TRUE-79401 needed **no** schema/dbchanges migration
|
|
253
|
-
(the earlier fear that it was ~VARCHAR(55) was wrong)
|
|
287
|
+
(the earlier fear that it was ~VARCHAR(55) was wrong) — but **it is a real ceiling: ~6–7
|
|
288
|
+
business-length addresses** (measured: 3 = 145 chars, 5 = 243 fit; 8 = 390, 10 = 489 overflow).
|
|
289
|
+
The 2.0 collection is unbounded; this column is not. See *Storage is bounded* above. `TOGA_AIG` is the 1.0 legacy DB;
|
|
254
290
|
`Client_Aig` is the 2.0 prod tenant.
|
|
255
291
|
- **The multi-email path is not yet exercised in prod data (TRUE-79401)** — as of 2026-07-28 all 20
|
|
256
292
|
`Client_Aig.ContactEmailAddresses` rows are one-per-contact. The *payload-shape* question that
|
|
@@ -280,10 +316,48 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
280
316
|
was ~VARCHAR(55)" traces to a garbled transcript line — "needs to fifty-five… maybe we should
|
|
281
317
|
change it to a long"; the column is `varchar(255)`. Closed.)
|
|
282
318
|
- **PHP 7.2** target (prod worker/library) — no arrow functions, typed properties, `??=`, `match`.
|
|
283
|
-
Lint with `
|
|
319
|
+
Lint with your **PHP 7.2 CLI** (`php -l <file>`); do not lint 1.0 code with a PHP 8 binary. (The
|
|
320
|
+
per-machine location of each PHP CLI is a developer-local detail and is deliberately not recorded
|
|
321
|
+
here.)
|
|
322
|
+
- **This logic IS testable without a database — and there is a harness.**
|
|
323
|
+
`App_Database::sqlEscape()` needs **no DB connection** (pure `str_replace`), so the email
|
|
324
|
+
transform can be exercised statically. The TRUE-79401 transform is inlined in the DB-writing
|
|
325
|
+
`private static syncContactToToga1Customer()`, so the harness **mirrors** it and guards against
|
|
326
|
+
drift by asserting the production statements verbatim — see
|
|
327
|
+
[static no-DB regression harness](../../test/features/static-no-db-regression-harness.md). **If
|
|
328
|
+
you edit those 5 statements, the harness will refuse to report until it is updated.** The clean
|
|
329
|
+
fix if you are already in there: extract the transform to a `public static` helper so the mirror
|
|
330
|
+
disappears.
|
|
331
|
+
- **Pre-existing (low severity, out of scope): a raw-vs-escaped comparison causes a redundant
|
|
332
|
+
UPDATE every sync.** At the update path (~line 8584) `syncContactToToga1Customer` compares
|
|
333
|
+
`$existingCustomer['emailAddress'] !== $emailAddress` — the DB value from
|
|
334
|
+
`App_Database::fetchRow()` is **unescaped**, while `$emailAddress` has already been through
|
|
335
|
+
`App_Database::sqlEscape()`. For any value containing `'`, `"` or `\` the two can **never** compare
|
|
336
|
+
equal, so that column is rewritten on **every** run. Same pattern applies to `businessName` and
|
|
337
|
+
`phoneNumber` just above. **Predates TRUE-79401**, is a redundant write (not data loss or
|
|
338
|
+
corruption), and was deliberately left out of that ticket's scope — recorded here so a future
|
|
339
|
+
reader knows it is known.
|
|
284
340
|
|
|
285
341
|
## Change history
|
|
286
342
|
|
|
343
|
+
- 2026-08-03 (later pass) — TRUE-79401 **static regression harness + one correction.** BUILT a
|
|
344
|
+
no-DB PHP harness (`test/@Mark/AIG/test_multi_email.php`, 12 cases, PHP 7.2.33, `php -l` clean)
|
|
345
|
+
that feeds mocked `/contacts` fragments through the email transform and asserts the delimited
|
|
346
|
+
string; it **mirrors** the transform (inlined in the DB-writing private method) and **guards
|
|
347
|
+
against drift** by asserting the 5 production statements verbatim, plus parses
|
|
348
|
+
`CUSTOMER_EMAIL_DELIMITER` from source — technique documented in
|
|
349
|
+
[static no-DB regression harness](../../test/features/static-no-db-regression-harness.md).
|
|
350
|
+
**CORRECTION (supersedes the earlier same-day entry):** a bare-string collection does **not**
|
|
351
|
+
blank/vanish the addresses — it **silently degrades to primary-only**, byte-identical to correct
|
|
352
|
+
single-email output, so the wrong shape is indistinguishable from the feature being disabled;
|
|
353
|
+
child row counts in `Client_Aig.ContactEmailAddresses` are the only signal. Also recorded: the
|
|
354
|
+
omitted-primary sub-case (repeat the primary in the array); the **`varchar(255)` ceiling of ~6–7
|
|
355
|
+
business-length addresses** (so "send as many as you like" is false at the 1.0 layer, qualifying
|
|
356
|
+
the earlier pitch); the pre-existing raw-vs-escaped comparison causing a redundant UPDATE every
|
|
357
|
+
sync (~line 8584, out of scope); genericized the lint instruction (was a non-existent absolute
|
|
358
|
+
`C:\xampp7\...` path); and **verified the TRUE-79401 branch is fully merged with `_production`
|
|
359
|
+
with its patch intact at +17/−2 under ~404 insertions of other tickets' merge noise** (deploy to
|
|
360
|
+
the prod worker still unverified). (mhammontree)
|
|
287
361
|
- 2026-08-03 — TRUE-79401 **payload shape RESOLVED** (review/no-code session; sources: AIG's
|
|
288
362
|
field-mapping spreadsheet, the 2026-05-01 Teams meeting, the 2026-05-04 AIG email). Both
|
|
289
363
|
`UserDefined3` and `UserDefined4` map to the **same** nested
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
|
|
10
10
|
| [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
|
|
11
11
|
| [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
|
|
12
|
+
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, library/app/database.php, library/app/api/toga2.php |
|
|
12
13
|
| [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
|
|
13
14
|
| [Talos Knowledge Base Pipeline (Uploader + Processor)](features/talos-kb-pipeline.md) | `team/talos/` holds the two-script web tooling that feeds the **TOGa Talos** (TOGa IQ) AI knowledge bases. | test/team/talos/kb_uploader.php, test/team/talos/kb_processor.php, test/team/talos/kb_processor.ini |
|
|
14
15
|
| [TOGa 2.0 Client Onboarding SQL Generator](features/toga2-client-onboarding-sql.md) | > **Superseded by the browser wizard.** The generation logic here was extracted into the reusable > `OnboardingSqlGenerator` class and wrapped in a local browse | test/team/generate_toga2_onboarding_sql.php |
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: test
|
|
5
|
+
project: Test
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- test/@Mark/AIG/test_multi_email.php
|
|
13
|
+
- library/app/database.php
|
|
14
|
+
- library/app/api/toga2.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../library/features/toga2-api-client-and-bridge.md
|
|
18
|
+
- ../../library/architecture.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL,
|
|
24
|
+
so "just call it" means standing up a client database. This doc records the technique that makes
|
|
25
|
+
a large class of 1.0 logic testable **statically — no DB connection, no framework bootstrap** —
|
|
26
|
+
plus the **source drift guard** that keeps such a test honest when the logic under test cannot
|
|
27
|
+
actually be invoked.
|
|
28
|
+
|
|
29
|
+
First built for TRUE-79401 (`test/@Mark/AIG/test_multi_email.php`): 12 cases feeding mocked 2.0
|
|
30
|
+
`/contacts` payload fragments through the multi-email transform and asserting the delimited
|
|
31
|
+
string that would be written to `TOGA_AIG.Customers.emailAddress`. Runs on PHP 7.2.33, `php -l`
|
|
32
|
+
clean, no database.
|
|
33
|
+
|
|
34
|
+
## Key enabler: `App_Database::sqlEscape()` needs NO database connection
|
|
35
|
+
|
|
36
|
+
This is the fact that unlocks static 1.0 testing and it is documented nowhere else:
|
|
37
|
+
|
|
38
|
+
- `App_Database::sqlEscape()` delegates to `App_Database::mysqlRealEscapeString()`, which is a
|
|
39
|
+
**pure `str_replace`** over `\`, `\x00`, `\n`, `\r`, `'`, `"`, `\x1a`. The
|
|
40
|
+
`mysqli_real_escape_string` implementation is **commented out** in the source.
|
|
41
|
+
- So it needs **no mysqli link and no `App_Registry` lookup**.
|
|
42
|
+
- `library/app/database.php` declares `abstract class App_Database` with **no `extends`** and no
|
|
43
|
+
parse-time dependencies, so a test can `require_once` that one file alone.
|
|
44
|
+
|
|
45
|
+
Consequence: **any 1.0 logic whose only framework dependency is escaping is unit-testable
|
|
46
|
+
statically.** Consequence for correctness testing too — because escaping is a pure string
|
|
47
|
+
transform, a test can assert the *exact* escaped output.
|
|
48
|
+
|
|
49
|
+
## How the harness is structured
|
|
50
|
+
|
|
51
|
+
1. `require_once` `library/app/database.php` only (no bootstrap, no config, no DB).
|
|
52
|
+
2. Build mocked payload fragments as plain `stdClass`/arrays shaped like the 2.0 API response.
|
|
53
|
+
3. Run the transform (see drift guard below) and assert the resulting string.
|
|
54
|
+
4. Cases cover: single address, multiple, duplicates, blank/whitespace entries, empty
|
|
55
|
+
collection (primary fallback), and wrong payload shapes.
|
|
56
|
+
5. Parse `CUSTOMER_EMAIL_DELIMITER` **out of the production source** rather than hardcoding
|
|
57
|
+
`'; '`, so changing the constant does not silently invalidate the expectations.
|
|
58
|
+
|
|
59
|
+
## Drift guard — for logic that cannot be called
|
|
60
|
+
|
|
61
|
+
The TRUE-79401 transform is 3 lines **inlined inside `private static
|
|
62
|
+
App_Api_Toga2::syncContactToToga1Customer()`**, which also writes SQL. It cannot be invoked
|
|
63
|
+
without a live client DB, so the harness must **mirror** it — and a mirror silently rots.
|
|
64
|
+
|
|
65
|
+
The guard: the harness **reads `library/app/api/toga2.php`**, normalizes whitespace, and asserts
|
|
66
|
+
that the **5 production statements it mirrors still appear verbatim**. On any mismatch it prints
|
|
67
|
+
`DRIFT DETECTED` and **refuses to report results at all** (it does not just warn — a passing
|
|
68
|
+
mirror of stale code is worse than no test).
|
|
69
|
+
|
|
70
|
+
This pattern is reusable for **any 1.0 pure-logic fragment buried in a DB-writing private
|
|
71
|
+
method**, which is a large share of `library`.
|
|
72
|
+
|
|
73
|
+
**Cleaner long-term alternative (deliberately NOT done here):** extract the transform into a
|
|
74
|
+
`public static` helper so the test calls production code directly and the mirror disappears
|
|
75
|
+
entirely. That was out of scope because it changes production code; prefer it when you are
|
|
76
|
+
already editing the method.
|
|
77
|
+
|
|
78
|
+
## Gotchas / known issues
|
|
79
|
+
|
|
80
|
+
- **A mirrored test proves the mirror, not production.** The drift guard is what makes it
|
|
81
|
+
trustworthy — never ship a mirror without one.
|
|
82
|
+
- **Static tests cannot validate anything the server decides.** Column truncation, collation
|
|
83
|
+
behavior, strict mode and table-name casing are all server-config dependent — a no-DB pass says
|
|
84
|
+
nothing about them. See
|
|
85
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
86
|
+
- **`@Mark/` (and any `@<name>/`) folders in `test` are per-developer.** `test` is not branched;
|
|
87
|
+
write only in your own folder and ask before touching another developer's.
|
|
88
|
+
- Lint 1.0 code with your **PHP 7.2** CLI (`php -l <file>`) — the prod worker/library target is
|
|
89
|
+
7.2, so 7.4+/8.x syntax must not appear.
|
|
90
|
+
|
|
91
|
+
## Change history
|
|
92
|
+
|
|
93
|
+
- 2026-08-03 — Documented the static no-DB 1.0 harness technique, built for TRUE-79401
|
|
94
|
+
(`test/@Mark/AIG/test_multi_email.php`, 12 cases, PHP 7.2.33). Key enabler recorded:
|
|
95
|
+
`App_Database::sqlEscape()` is a pure `str_replace` (the `mysqli_real_escape_string` path is
|
|
96
|
+
commented out) needing **no DB link or registry**, and `library/app/database.php` can be
|
|
97
|
+
`require_once`d alone. Also recorded the **source drift guard** for logic inlined in a
|
|
98
|
+
DB-writing private method (assert the mirrored production statements appear verbatim; refuse to
|
|
99
|
+
report results on mismatch) and parsing constants out of the source instead of hardcoding them.
|
|
100
|
+
(mhammontree)
|
|
@@ -135,9 +135,20 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
135
135
|
`UserDefined3`) instead of the nested collection. The key is dropped; no child row is created.
|
|
136
136
|
2. **Array of bare strings instead of array of objects.** `"contactEmailAddresses":
|
|
137
137
|
["email2@domain.com"]` instead of `[{"emailAddress": "email2@domain.com"}]`. Nothing is
|
|
138
|
-
created, and any consumer doing `array_column($list, 'emailAddress')` gets an **empty** list
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
created, and any consumer doing `array_column($list, 'emailAddress')` gets an **empty** list.
|
|
139
|
+
|
|
140
|
+
**Corrected 2026-08-03 (proven by harness):** the downstream effect is **not** an empty/blanked
|
|
141
|
+
field. The empty list makes the consumer's **primary-email fallback fire**, so it stores
|
|
142
|
+
**exactly the primary address** — output that is **byte-identical to correct single-email
|
|
143
|
+
behavior**. There is therefore **no observable difference between "the partner sent the wrong
|
|
144
|
+
shape" and "the partner hasn't enabled the extra addresses yet."** That is strictly *worse* for
|
|
145
|
+
diagnosis than a blanked field would be, and it is why the child ROW COUNT is the only reliable
|
|
146
|
+
signal (see below) — the consumer's stored value cannot distinguish the two.
|
|
147
|
+
|
|
148
|
+
Related shape trap in the same family: if the partner sends extras but **omits the primary from
|
|
149
|
+
the collection**, the primary is **not re-added** — the fallback only fires on a *fully empty*
|
|
150
|
+
list — so the consumer stores the secondary/tertiary but **not** the main address. Tell partners
|
|
151
|
+
to **repeat the primary inside the array**.
|
|
141
152
|
|
|
142
153
|
**Operational rules that follow:**
|
|
143
154
|
|
|
@@ -163,6 +174,15 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
163
174
|
|
|
164
175
|
## Change history
|
|
165
176
|
|
|
177
|
+
- 2026-08-03 (later pass) — **Correction, supersedes the entry below on one point.** The bare-string
|
|
178
|
+
collection shape does **not** make the addresses "vanish"/blank the consumer's field: the empty
|
|
179
|
+
`array_column()` result makes the consumer's primary-email fallback fire, so the stored value is
|
|
180
|
+
**byte-identical to correct single-email output** and the wrong shape is **indistinguishable from
|
|
181
|
+
the feature simply not being enabled** — worse for diagnosis, and confirmation that the child row
|
|
182
|
+
count is the only reliable signal. Also added the omitted-primary sub-case (the fallback fires
|
|
183
|
+
only on a *fully empty* list, so a collection lacking the primary stores the extras **without**
|
|
184
|
+
the main address — partners should repeat the primary inside the array). Proven by the TRUE-79401
|
|
185
|
+
static harness. (mhammontree)
|
|
166
186
|
- 2026-08-03 — Documented that V2 **silently ignores unrecognized payload keys and does not
|
|
167
187
|
validate nested-collection element shape**: both a flat/unknown key and an array of bare strings
|
|
168
188
|
(instead of an array of objects) return **HTTP 201** with the data dropped and no error logged
|
|
@@ -5,4 +5,5 @@
|
|
|
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
6
|
| [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/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 |
|
|
7
7
|
| [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
|
+
| [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
|
|
8
9
|
| [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: [jcardinal, mhammontree, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -417,6 +417,42 @@ under the 64 MB `max_allowed_packet` default. First hit in
|
|
|
417
417
|
`Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql` (2,200 AIG SaleItem codes); see
|
|
418
418
|
`clients/aig/features/entitlement-intake.md` → *Uploading new codes*.
|
|
419
419
|
|
|
420
|
+
### Staging tables MUST pin the collation of the column they join against
|
|
421
|
+
|
|
422
|
+
A `CREATE TEMPORARY TABLE` that omits an explicit `COLLATE` inherits the **connection/database
|
|
423
|
+
default**, which is **not** guaranteed to match the target column. The moment the staging column
|
|
424
|
+
is joined to a real column with a different collation, MySQL raises:
|
|
425
|
+
|
|
426
|
+
ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and
|
|
427
|
+
(utf8mb4_unicode_ci,IMPLICIT) for operation '='
|
|
428
|
+
|
|
429
|
+
The connection default is commonly `utf8mb4_general_ci`, while 2.0 tenant tables are **typically**
|
|
430
|
+
`utf8mb4_unicode_ci` (verified for `Client_Aig.Items.partNumber`) — so the mismatch is the default
|
|
431
|
+
outcome, not the exception. **Always declare the collation explicitly on every staging column that
|
|
432
|
+
participates in a join**, copying it from the column being matched:
|
|
433
|
+
|
|
434
|
+
```sql
|
|
435
|
+
CREATE TEMPORARY TABLE _stage (
|
|
436
|
+
uuid VARCHAR(36) NOT NULL,
|
|
437
|
+
keyColumn VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL, -- pinned to TargetTable.keyColumn
|
|
438
|
+
otherColumn TEXT NULL
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**This is a per-machine failure, not a per-query one.** Whether it fires depends on the server's
|
|
443
|
+
own defaults, so the *same file* passes on one developer's machine and dies on another's — and on
|
|
444
|
+
the production server. Measured: it ran clean on MySQL **8.0.39** and failed with 1267 on
|
|
445
|
+
**8.0.44** with no change to the SQL. The same class of per-machine divergence applies to
|
|
446
|
+
**`sql_mode`** (e.g. `ONLY_FULL_GROUP_BY`/strict-mode rejecting a statement that passed elsewhere),
|
|
447
|
+
**`lower_case_table_names`** (table-name casing resolving locally and 404-ing on Linux), and
|
|
448
|
+
**`thread_stack`** (the `UNION ALL` parse limit above triggering at different sizes).
|
|
449
|
+
|
|
450
|
+
**Rule: a local pass is NOT evidence that a migration is portable.** Anything that depends on a
|
|
451
|
+
server variable must be pinned **in the SQL file itself** — explicit `COLLATE`, explicit column
|
|
452
|
+
casing, no reliance on a permissive `sql_mode`. Verify server-setting parity before trusting a
|
|
453
|
+
local run; see the environment-parity workflow in
|
|
454
|
+
`2.0/apps/_underscore/architecture.md` → *Database architecture*.
|
|
455
|
+
|
|
420
456
|
## Self-referencing DELETE — wrap the subquery in a derived table
|
|
421
457
|
|
|
422
458
|
MySQL forbids a `DELETE` (or `UPDATE`) whose `WHERE` subquery reads **the same table being
|
|
@@ -511,6 +547,15 @@ defined in `2.0/apps/_underscore/architecture.md`, and its change files create/a
|
|
|
511
547
|
tables that `_Model_*` classes map to.
|
|
512
548
|
|
|
513
549
|
## Change history
|
|
550
|
+
- 2026-08-03 — **Added *Staging tables MUST pin the collation of the column they join against*.**
|
|
551
|
+
A `CREATE TEMPORARY TABLE` without an explicit `COLLATE` inherits the connection default
|
|
552
|
+
(commonly `utf8mb4_general_ci`) and dies with **error 1267 illegal mix of collations** when
|
|
553
|
+
joined to a tenant column that is typically `utf8mb4_unicode_ci` (verified for
|
|
554
|
+
`Client_Aig.Items.partNumber`). Pin the collation on every staging column used in a join.
|
|
555
|
+
Reframed as the general rule that **a local pass is not evidence of portability**: the same file
|
|
556
|
+
ran clean on MySQL 8.0.39 and failed on 8.0.44 unchanged, and the same per-machine divergence
|
|
557
|
+
applies to `sql_mode`, `lower_case_table_names`, and `thread_stack`. Anything depending on a
|
|
558
|
+
server variable must be pinned in the SQL file itself. (mhammontree)
|
|
514
559
|
- 2026-07-31 — **Added *`Core.Records` / `Core.RecordFields` — the only hardcoded `id`s on the
|
|
515
560
|
platform* + rule #9.** These two tables are the **only** ones platform-wide whose `id` is a
|
|
516
561
|
team-maintained constant: the next available value is tracked **in the developer chat**, so a
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Local vs prod MySQL config parity — why “it passed locally” is not evidence"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
related:
|
|
12
|
+
- ../architecture.md
|
|
13
|
+
- ../workflows/nonprod-metadata-drift-repair.md
|
|
14
|
+
- ../../../clients/aig/features/entitlement-intake.md
|
|
15
|
+
- ../../../1.0/apps/test/features/static-no-db-regression-harness.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL
|
|
21
|
+
server-configuration differences**. They are not version differences and not environment
|
|
22
|
+
properties — prod and local are both **MySQL 8.0** — so the same SQL can pass on one developer's
|
|
23
|
+
machine, fail on another's, and fail on deploy, with no code change involved.
|
|
24
|
+
|
|
25
|
+
Measured 2026-08-03: **prod Core = 8.0.39**, **local Core_2 = 8.0.44**. Same engine, same major
|
|
26
|
+
version.
|
|
27
|
+
|
|
28
|
+
**Operating rule: a local pass is NOT evidence that a migration is safe.** Run the parity query
|
|
29
|
+
below before trusting a local result, and pin anything the server would otherwise decide for you.
|
|
30
|
+
|
|
31
|
+
## Parity check — run this on BOTH the target environment and locally
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
SELECT VERSION(), @@collation_server, @@character_set_server, @@sql_mode,
|
|
35
|
+
@@lower_case_table_names, @@thread_stack, @@max_allowed_packet;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Compare the two outputs before concluding anything from a local run. (Machine-specific config
|
|
39
|
+
file locations and one developer's overrides are deliberately **not** recorded here — they belong
|
|
40
|
+
in per-developer notes, not the shared KB.)
|
|
41
|
+
|
|
42
|
+
## 1. `collation_server` — the ERROR 1267 trap is per-MACHINE, not prod-only
|
|
43
|
+
|
|
44
|
+
Measured: **prod `collation_server = utf8mb4_0900_ai_ci`** (the MySQL 8 *default*); **local
|
|
45
|
+
`utf8mb4_unicode_ci`** (an explicit config override on that machine).
|
|
46
|
+
|
|
47
|
+
So the difference is a **config property of whichever server was left at the MySQL 8 default** —
|
|
48
|
+
*not* a property of "production". Consequences:
|
|
49
|
+
|
|
50
|
+
- A developer on a **stock MySQL 8** install **will** reproduce `ERROR 1267 (Illegal mix of
|
|
51
|
+
collations)` locally, and is actively misled by any doc claiming it only happens on prod.
|
|
52
|
+
- A developer with the `utf8mb4_unicode_ci` override will **never** reproduce it.
|
|
53
|
+
- It is **fragile**: a MySQL reinstall or a reset config file silently flips the behavior with no
|
|
54
|
+
code change.
|
|
55
|
+
|
|
56
|
+
**Mitigation (unchanged, now correctly justified):** always pin
|
|
57
|
+
`CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` **explicitly** on temp/staging columns that
|
|
58
|
+
will be compared to a real table column. Because the server default is per-machine, **a local
|
|
59
|
+
pass proves nothing** about the deploy target. First hit on the AIG SaleItem load — see
|
|
60
|
+
[AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md) → *Uploading new
|
|
61
|
+
codes*.
|
|
62
|
+
|
|
63
|
+
## 2. `sql_mode` — an empty local mode truncates where prod throws ERROR 1406
|
|
64
|
+
|
|
65
|
+
MySQL 8's default `sql_mode` includes **`STRICT_TRANS_TABLES`** and **`ONLY_FULL_GROUP_BY`**. A
|
|
66
|
+
local instance configured with an **empty `sql_mode`**:
|
|
67
|
+
|
|
68
|
+
- **Silently TRUNCATES** an over-length write (e.g. a >255-char value into a `varchar(255)`)
|
|
69
|
+
where a strict server raises **`ERROR 1406 Data too long`**.
|
|
70
|
+
- Accepts `GROUP BY` queries that a strict (`ONLY_FULL_GROUP_BY`) server **rejects**.
|
|
71
|
+
|
|
72
|
+
Directly relevant to the 1.0 multi-email work: overflow of `Customers.emailAddress` behaves
|
|
73
|
+
**differently per machine** — truncate here, hard error there. See
|
|
74
|
+
[the library bridge doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
|
|
75
|
+
|
|
76
|
+
## 3. `lower_case_table_names` — a PERMANENT Windows-local vs Linux-prod difference
|
|
77
|
+
|
|
78
|
+
- Windows dev installs typically run **`lower_case_table_names=2`**; Linux prod runs **`0`**
|
|
79
|
+
(case-**sensitive** table names).
|
|
80
|
+
- It **cannot be changed after the data directory is initialized** — MySQL 8 refuses to start on a
|
|
81
|
+
data-dictionary mismatch — and `0` is **not permitted on Windows at all**. This one is not
|
|
82
|
+
fixable, only worked around.
|
|
83
|
+
- Consequence: SQL referencing a table with the wrong casing (e.g. `contactemailaddresses` instead
|
|
84
|
+
of `ContactEmailAddresses`) **passes locally and fails on prod.**
|
|
85
|
+
|
|
86
|
+
2.0 table names are **PascalCase**, so treat exact table-name casing as something local testing
|
|
87
|
+
can **never** validate. Review it by eye.
|
|
88
|
+
|
|
89
|
+
## 4. `thread_stack` — a bigger local stack hides ERROR 1436 until deploy
|
|
90
|
+
|
|
91
|
+
`thread_stack` is what produced the **`ERROR 1436 Thread stack overrun`** on the ~2,200-row
|
|
92
|
+
`UNION ALL` in TRUE-79534. A larger local stack means a big migration **parses locally and dies on
|
|
93
|
+
deploy** — the same shape as the collation trap. The fix is still structural (flat multi-row
|
|
94
|
+
`VALUES` into a temp table, never a long `UNION ALL` chain); see
|
|
95
|
+
[dbchanges2 architecture](../architecture.md) → *Bulk data loads*. Do not raise `thread_stack` for
|
|
96
|
+
a one-off load.
|
|
97
|
+
|
|
98
|
+
## Gotchas / known issues
|
|
99
|
+
|
|
100
|
+
- **The common failure mode is a false negative, not a false positive.** Every item above lets bad
|
|
101
|
+
SQL pass locally. Nothing here makes good SQL fail on prod.
|
|
102
|
+
- **Do not "fix" a parity gap by changing the server.** Pin collations in the SQL, keep casing
|
|
103
|
+
exact, size values to the column, and structure bulk loads to avoid parser recursion.
|
|
104
|
+
|
|
105
|
+
## Change history
|
|
106
|
+
|
|
107
|
+
- 2026-08-03 — Created. Established that the `ERROR 1267` temp-table collation trap is a
|
|
108
|
+
**per-machine config property, not a prod-only / version difference** (measured prod 8.0.39 with
|
|
109
|
+
`utf8mb4_0900_ai_ci`, local 8.0.44 with an explicit `utf8mb4_unicode_ci` override) — a developer
|
|
110
|
+
on a stock MySQL 8 install reproduces it locally. Added the `sql_mode` finding (empty local mode
|
|
111
|
+
silently truncates where strict prod raises `ERROR 1406`, and disables `ONLY_FULL_GROUP_BY`), the
|
|
112
|
+
permanent `lower_case_table_names` Windows-vs-Linux table-name-casing gap, the `thread_stack`
|
|
113
|
+
connection to `ERROR 1436`, and the cross-environment parity query. (mhammontree)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -12,7 +12,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
12
12
|
- **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
13
13
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
15
|
-
- **test** (Test) —
|
|
15
|
+
- **test** (Test) — 14 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
17
|
- **tools** (Tools) — 13 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 42 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 38 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 21 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 5 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -227,12 +227,15 @@ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Des
|
|
|
227
227
|
The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect
|
|
228
228
|
even if the run aborts). Do **not** raise `thread_stack` on the server for a one-off load.
|
|
229
229
|
5. **Pin the staging `partNumber` collation explicitly to `utf8mb4_unicode_ci`** (as shown
|
|
230
|
-
above). If you omit it, the temp column inherits the **server default
|
|
231
|
-
|
|
232
|
-
`utf8mb4_unicode_ci`. The anti-join's
|
|
233
|
-
collations and dies with **ERROR 1267
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
above). If you omit it, the temp column inherits the **server default**. On any server left at
|
|
231
|
+
the MySQL 8 default that is `utf8mb4_0900_ai_ci` — including **prod** — while
|
|
232
|
+
`Client_Aig.Items.partNumber` is `utf8mb4_unicode_ci`. The anti-join's
|
|
233
|
+
`existing.partNumber = src.partNumber` then mixes collations and dies with **ERROR 1267
|
|
234
|
+
(Illegal mix of collations)**. This is a **per-machine config** property, **not** a prod-only or
|
|
235
|
+
version difference (prod and local are both MySQL 8.0) — a developer on a stock MySQL 8 install
|
|
236
|
+
reproduces it locally, and a developer with a `utf8mb4_unicode_ci` override never does. So **a
|
|
237
|
+
local pass is not evidence.** See the gotcha and
|
|
238
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
236
239
|
|
|
237
240
|
## Client variations
|
|
238
241
|
|
|
@@ -252,7 +255,12 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
252
255
|
Both a flat/unknown key (a literal `UserDefined3`) and an array of bare strings
|
|
253
256
|
(`"contactEmailAddresses": ["a@b.com"]` instead of `[{"emailAddress": "a@b.com"}]`) are accepted
|
|
254
257
|
with **no error anywhere**. When verifying AIG's multi-email feed, assert **actual
|
|
255
|
-
`Client_Aig.ContactEmailAddresses` row counts** — never the HTTP status
|
|
258
|
+
`Client_Aig.ContactEmailAddresses` row counts** — never the HTTP status, **and (corrected
|
|
259
|
+
2026-08-03) never the 1.0 `TOGA_AIG.Customers.emailAddress` value either.** Proven by harness:
|
|
260
|
+
the bare-string shape does **not** blank the 1.0 field — `array_column()` returns an empty list,
|
|
261
|
+
the primary-email fallback fires, and the stored value is **byte-identical to correct
|
|
262
|
+
single-email output**, so "AIG sent the wrong shape" and "AIG hasn't enabled the feature yet"
|
|
263
|
+
are indistinguishable. The child row count is the **only** reliable signal. See
|
|
256
264
|
[api2 nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
257
265
|
- **A sale-item code missing from `Client_Aig.Items` currently produces an HTTP 500, not a clean
|
|
258
266
|
"Missing AIG item ID."** In prod (TRUE-79534) a valid-but-unknown code made `$itemId` empty and
|
|
@@ -295,17 +303,34 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
295
303
|
type. **Both stem from the same unguarded lookup** — `prePost` must guard an empty/failed lookup
|
|
296
304
|
and return a clean "Missing AIG item ID" **before** running the fulfillment query or injecting
|
|
297
305
|
anything. Not yet fixed.
|
|
298
|
-
- **Staging a temp table for a `Client_Aig` load can hit ERROR 1267 on
|
|
299
|
-
|
|
300
|
-
`
|
|
301
|
-
`
|
|
302
|
-
|
|
303
|
-
**
|
|
304
|
-
`
|
|
305
|
-
|
|
306
|
+
- **Staging a temp table for a `Client_Aig` load can hit ERROR 1267 — on whichever server is left
|
|
307
|
+
at the MySQL 8 default collation (a PER-MACHINE config trap, not "prod-only").** A
|
|
308
|
+
`CREATE TEMPORARY TABLE` column with no explicit collation inherits `@@collation_server`, which
|
|
309
|
+
is `utf8mb4_0900_ai_ci` by default on MySQL 8 (prod is exactly this) — while
|
|
310
|
+
`Client_Aig.Items.partNumber` is `utf8mb4_unicode_ci`; the anti-join `=` then mixes collations →
|
|
311
|
+
**ERROR 1267 (Illegal mix of collations)**. Measured 2026-08-03: prod Core **8.0.39 /
|
|
312
|
+
`utf8mb4_0900_ai_ci`**, local Core_2 **8.0.44 / `utf8mb4_unicode_ci` (an explicit override)** —
|
|
313
|
+
same engine, same major version, so this was **never** a version or environment difference. A
|
|
314
|
+
developer on a stock MySQL 8 install **will** reproduce it locally; one with the override never
|
|
315
|
+
will; a MySQL reinstall silently flips it. Always pin the staging `partNumber` column
|
|
316
|
+
`CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` (see *Uploading new codes* step 5) **and treat
|
|
317
|
+
a local pass as no evidence at all** — see
|
|
318
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
319
|
+
This is a general dbchanges2 temp-table-staging rule, not AIG-specific.
|
|
306
320
|
|
|
307
321
|
## Change history
|
|
308
322
|
|
|
323
|
+
- 2026-08-03 (later pass) — **Two corrections.** (1) The `ERROR 1267` temp-table collation trap is a
|
|
324
|
+
**per-machine server-config property, not a "prod-only" / version difference**: measured prod Core
|
|
325
|
+
8.0.39 with the MySQL 8 default `utf8mb4_0900_ai_ci` vs local Core_2 8.0.44 with an explicit
|
|
326
|
+
`utf8mb4_unicode_ci` override. The earlier reasoning ("the local server default is already
|
|
327
|
+
`utf8mb4_unicode_ci`") was wrong even though the mitigation was right — a developer on a stock
|
|
328
|
+
MySQL 8 install reproduces it locally, so **a local pass is not evidence.** New doc:
|
|
329
|
+
[local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
|
|
330
|
+
(2) The wrong-shaped-payload gotcha overstated the symptom: an array of **bare strings** does
|
|
331
|
+
**not** blank the 1.0 email field — it **silently degrades to primary-only**, producing output
|
|
332
|
+
byte-identical to correct single-email behavior, so it is undetectable from the stored value.
|
|
333
|
+
`Client_Aig.ContactEmailAddresses` row counts remain the only reliable signal. (mhammontree)
|
|
309
334
|
- 2026-08-03 — Review-only session (no code changed). **Business Unit (`UserDefined2`) is answered
|
|
310
335
|
and dropped**: AIG (2026-05-04) needs nothing returned — it is theirs, from the sale, for **sales**
|
|
311
336
|
invoicing (not claims invoicing); we asked them to omit it from the API call and **no invoice-file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: session
|
|
3
3
|
slug: true-79401-multi-email-resolution
|
|
4
|
-
title: TRUE-79401 —
|
|
4
|
+
title: TRUE-79401 — payload shape closed (no api2 work), static harness built, branch merged up
|
|
5
5
|
author: mhammontree
|
|
6
|
-
repos: [library, api2, _underscore, worker2]
|
|
6
|
+
repos: [library, test, api2, _underscore, worker2, dbchanges2]
|
|
7
7
|
framework: "both"
|
|
8
8
|
client: aig
|
|
9
9
|
status: active
|
|
@@ -13,70 +13,93 @@ updated: 2026-08-03
|
|
|
13
13
|
|
|
14
14
|
# Session: true-79401-multi-email-resolution
|
|
15
15
|
**Date:** 2026-08-03
|
|
16
|
-
**Project/Repo:** library (1.0 core) — with 2.0 findings in api2 / _underscore / worker2
|
|
17
|
-
**Task:**
|
|
16
|
+
**Project/Repo:** library (1.0 core) — with 2.0 findings in api2 / _underscore / worker2 / dbchanges2
|
|
17
|
+
**Task:** Resolve the last open question on TRUE-79401 (multiple email addresses for AIG in the 1.0 `library` bridge) — "do we need an api2 intake mapping?" — then build a static regression harness for the transform and bring the branch up to date with `_production`.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## What WORKED
|
|
22
22
|
|
|
23
|
-
- **Found the prior session's record immediately** via the knowledge base, not
|
|
24
|
-
- **Confirmed the library implementation is present
|
|
25
|
-
- **Proved `UserDefined3` is not a real field anywhere.** Grep across all of `C:\WWW` returned exactly two hits: the comment at `library/app/api/toga2.php:8424` and the knowledge doc itself. Zero hits in `api2` or `_underscore`.
|
|
26
|
-
- **Proved `c_businessUnit` was never built.** Grep for `businessUnit|c_businessUnit` (case-insensitive) across `C:\WWW` returned only unrelated `BusinessUnits`
|
|
27
|
-
- **Extracted the Teams transcript from
|
|
28
|
-
- **The transcript answered the question**, plus three things
|
|
29
|
-
- **AIG's mapping spreadsheet (
|
|
30
|
-
-
|
|
23
|
+
- **Found the prior session's record immediately** via the knowledge base, not code archaeology. `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` held the full TRUE-79401 history from the 2026-07-28 capture, including the open question and the "owner Paulina" note.
|
|
24
|
+
- **Confirmed the library implementation is present** — grep for `CUSTOMER_EMAIL_DELIMITER|contactEmailAddresses` in `C:\WWW\library` returned `toga2.php:112` (the `'; '` constant), `:8424` (the `/contacts` fetch `fields` whitelist entry), and `:8499–8505` (the `array_column` → trim/filter/unique → implode → `sqlEscape` chain).
|
|
25
|
+
- **Proved `UserDefined3` is not a real field anywhere.** Grep across all of `C:\WWW` returned exactly two hits: the comment at `library/app/api/toga2.php:8424` and the knowledge doc itself. Zero hits in `api2` or `_underscore`. Key evidence that the flat-field branch was never implemented and never intended.
|
|
26
|
+
- **Proved `c_businessUnit` was never built.** Grep for `businessUnit|c_businessUnit` (case-insensitive) across `C:\WWW` returned only unrelated `BusinessUnits` hits in the `forecast` repo — nothing in `_underscore`, `api2`, or `dbchanges2`.
|
|
27
|
+
- **Extracted the 2026-05-01 Teams transcript from a `.docx`** after two failures (below). Working method: `[System.IO.Compression.ZipFile]::ExtractToDirectory()` on the `.docx`, then on `word/document.xml` replace `</w:p>` → newline and `<w:br/>` → newline, strip `<[^>]+>`, and hand-replace the five XML entities. Produced 21,829 chars / 199 lines at `<scratchpad>/aig-transcript/transcript.txt`.
|
|
28
|
+
- **The transcript answered the question**, plus three unasked things: purpose is verification-only, the ~15-client-DB column approach was rejected, and the origin of the "VARCHAR(55)" fear.
|
|
29
|
+
- **AIG's mapping spreadsheet (image from Paulina) closed it definitively** — both `UserDefined3` and `UserDefined4` map to `Entitlements.contact.contactEmailAddresses[].emailAddress`, and the `UserDefined2` (Business Unit) target column is conspicuously **empty**.
|
|
30
|
+
- **Built a working static regression harness** at `test/@Mark/AIG/test_multi_email.php` — **12/12 cases pass** on PHP 7.2.33, `php -l` clean, no database required. Covers: 2 and 3 addresses, primary omitted from the collection, primary de-duplication, empty collection → primary fallback, absent collection property, no primary at all, bare-string array, whitespace trimming, empty-string filtering, mixed records/strings, and apostrophe escaping.
|
|
31
|
+
- **Discovered `App_Database::sqlEscape()` needs NO database connection** — it delegates to `App_Database::mysqlRealEscapeString()`, a pure `str_replace` over `\ \x00 \n \r ' " \x1a` (the `mysqli_real_escape_string` version is commented out). `abstract class App_Database` has no `extends` and no parse-time deps, so `require_once 'C:/WWW/library/app/database.php'` alone is enough. **This is what makes static 1.0 tests possible.**
|
|
32
|
+
- **The drift-guard technique works.** The transform is 3 lines inlined in a `private static` method that also writes SQL, so it can't be called without a DB and the test must mirror it. The harness reads `toga2.php`, normalizes whitespace, and asserts the 5 production statements still appear verbatim — printing DRIFT DETECTED and refusing to report results otherwise. It also parses `CUSTOMER_EMAIL_DELIMITER` out of the source instead of hardcoding `'; '`. Verified still OK after both merges.
|
|
33
|
+
- **Brought the branch fully up to date with `_production`.** `TRUE-79401` is now at `5aca31ea`; `git merge-base --is-ancestor _production HEAD` → YES; `git log HEAD.._production` empty; working tree clean; local `_production` == `origin/_production` as of the last fetch.
|
|
34
|
+
- **Confirmed the merge did not disturb the change.** The patch is **still +17 / −2** in `app/api/toga2.php` against `_production`. The two merge commits (`90b4178b`, `5aca31ea`) pulled in ~404 insertions of OTHER tickets' work and **zero** of them touched `syncContactToToga1Customer`, the `/contacts` fetch `fields` list, or any TRUE-79401 identifier (all merge hunks are below ~line 8000). `php -l` clean; harness 12/12 after merging.
|
|
35
|
+
- **`git diff _production...HEAD -- <file>` is the technique** that isolates a ticket's real contribution from merge noise when an old branch is finally synced.
|
|
36
|
+
- **Verified the variable split is complete.** Every remaining use of `$emailAddress` in `syncContactToToga1Customer` is a **storage** use (INSERT ~L8558, UPDATE ~L8584–8585); the only **matching** use is the dedupe `WHERE` now on `$primaryEmailAddress` (~L8532). No third site still matches against the joined list.
|
|
37
|
+
- **Measured prod vs local MySQL** — prod Core **8.0.39** `collation_server=utf8mb4_0900_ai_ci`; local Core_2 **8.0.44** `collation_server=utf8mb4_unicode_ci` (explicit `my.ini` override). Same engine, same major version.
|
|
38
|
+
- **Two `/capture` passes published and pushed** — 4 docs in the first, 6 in the second (including the ELEVATED `dbchanges2/architecture.md` after approval). Validate OK, index 307 docs / 30 repos, mirrored to `C:\WWW\.claude\knowledge`.
|
|
31
39
|
|
|
32
40
|
## What did NOT work — DO NOT RETRY THESE
|
|
33
41
|
|
|
34
|
-
- **Searching the production logs for a multi-email payload.**
|
|
35
|
-
- **Searching our own transcript store for the 2026-05-01 meeting.** `Team/Transcripts/Export` first ran
|
|
36
|
-
- **Reading the `.docx` with the `Read` tool.**
|
|
37
|
-
- **`[System.Web.HttpUtility]::HtmlDecode($t)` in the extraction script.**
|
|
38
|
-
-
|
|
42
|
+
- **Searching the production logs for a multi-email payload.** Dead end **by construction**: AIG sends only one address today and will not enable the extra fields until we tell them the feature is live. Every payload in `Logs_Aig.Api` and the core/writer `Logs` has exactly one `primaryContactEmailAddress`. There is nothing to find. The payload shape is a **specification** question, not a discovery question.
|
|
43
|
+
- **Searching our own transcript store for the 2026-05-01 meeting.** `Team/Transcripts/Export` first ran in production **2026-06-12** (PR #78), works off an incremental watermark with a bounded `lookbackDays`, and no multi-week backfill appears in its history. Pre-June meetings are absent from `Team.TranscriptExports`, `Team.TranscriptProcessing`, and the `togaiq` KBs despite being recorded. Recover them from Microsoft Graph or a manual export.
|
|
44
|
+
- **Reading the `.docx` with the `Read` tool.** Exact failure: `This tool cannot read binary files. The file appears to be a binary .docx file.`
|
|
45
|
+
- **`[System.Web.HttpUtility]::HtmlDecode($t)` in the extraction script.** Exact failure: `Unable to find type [System.Web.HttpUtility].` — Windows PowerShell 5.1 does not load `System.Web` by default. **The failure is NON-terminating**, so the script continued and wrote a partially-processed file that looked plausible (6 lines). Either `Add-Type -AssemblyName System.Web` first, or hand-replace `& < > " '`.
|
|
46
|
+
- **`C:\xampp7\php\php.exe` does not exist on this machine.** Exact failure: `Exit code 127 — C:/xampp7/php/php.exe: No such file or directory`. The knowledge doc said to lint with that path; it has since been genericized. The real binaries are `C:\xampp_7_2_33\php\php.exe` (PHP 7.2, for 1.0) and `C:\xampp_8x\php\php.exe` (PHP 8, for 2.0). Only the **8x MySQL** runs and it serves both.
|
|
47
|
+
- **My predicted behavior for a bare-string array was WRONG — and the truth is worse.** I expected `"contactEmailAddresses": ["email2@domain.com"]` to yield an empty stored value. The harness proved it yields **the primary address**: `array_column()` finds no `emailAddress` key → empty list → **the primary fallback fires**. The result is **byte-identical to today's correct single-email behavior**, so there is NO observable difference between "AIG sent the wrong shape" and "AIG hasn't enabled the feature yet." **Do not expect to detect a wrong payload shape by inspecting `TOGA_AIG.Customers.emailAddress`** — only a `Client_Aig.ContactEmailAddresses` row count distinguishes them. (An earlier `/capture` recorded the wrong version; corrected in a later pass.)
|
|
48
|
+
- **Counting the `varchar(255)` capacity overflow as a test failure.** It made the harness exit 1 on every run, which would train everyone to ignore it. Capacity is now tracked as an informational warning; only real regressions set the exit code.
|
|
49
|
+
- **Assuming `_production` was an ancestor after the first merge.** It was not — `git merge-base --is-ancestor _production HEAD` returned NO and merge-base was `871bc416`, because the local `_production` ref had advanced *after* that merge. Always re-check ancestry rather than trusting that a merge commit means "caught up."
|
|
39
50
|
|
|
40
51
|
## Not tried yet (candidates for next session)
|
|
41
52
|
|
|
42
|
-
- **The beta nested-write test.** POST a synthetic AIG entitlement to beta/QA with two nested `contactEmailAddresses` entries and **assert the actual `Client_Aig.ContactEmailAddresses` row count** — not the HTTP status. Must cover **both** a
|
|
43
|
-
- **
|
|
44
|
-
- **The note to Paulina / AIG** — two confirmations (did the mapping
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
53
|
+
- **The beta nested-write test.** POST a synthetic AIG entitlement to beta/QA with two nested `contactEmailAddresses` entries and **assert the actual `Client_Aig.ContactEmailAddresses` row count** — not the HTTP status, and (per the correction above) not the 1.0 stored string either. Must cover **both** a new contact (CREATE) and a **second entitlement for an existing contact** (UPDATE); the UPDATE path is the suspect one per `2.0/apps/api2/features/nested-relationship-writes.md` (`V2.php:4985–4996` reverse back-reference breaking single-key forced-MATCH, explicitly naming `primaryContactEmailAddress`).
|
|
54
|
+
- **Verify the library change is merged and DEPLOYED to the prod worker.** Still unverified — code confirmed in the working tree and on branch `TRUE-79401` only.
|
|
55
|
+
- **The note to Paulina / AIG** — retract the "only if necessary" ambiguity, include the concrete JSON example (not the `contactEmailAddresses[].emailAddress` path expression), and ask the two confirmations (did the mapping doc reach AIG after 2026-05-04; has their dev acknowledged the mapping).
|
|
56
|
+
- **Land the branch + harness** — `TRUE-79401` is 3 commits ahead of `_production` and the harness is unstaged. Nothing has been committed or pushed on the code side.
|
|
57
|
+
- **Harden the dedupe lookup** — `LIKE '%$primaryEmailAddress%'`, or drop the email fallback and rely on `c_togaCustomerId` alone.
|
|
58
|
+
- **Optionally extract the transform** into a `public static function buildCustomerEmailAddressList()` so the harness calls production directly and the mirror + drift guard become unnecessary. Deliberately NOT done — it changes production code.
|
|
59
|
+
- **`git fetch`** to confirm `origin/_production` hasn't moved since the last fetch (not run — network).
|
|
60
|
+
- **Guard the unrelated open `prePost` empty-`$itemId` defect** in `_Model_Aig_Entitlement` (a missing sale-item code still 500s instead of returning "Missing AIG item ID"). Pre-existing, out of TRUE-79401 scope.
|
|
47
61
|
|
|
48
62
|
## Current file state
|
|
49
63
|
|
|
50
64
|
| File | Status | Notes |
|
|
51
65
|
|------|--------|-------|
|
|
52
|
-
| `library/app/api/toga2.php` | **Unchanged
|
|
53
|
-
| `
|
|
54
|
-
| `_underscore/Model/
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `knowledge/
|
|
58
|
-
| `knowledge/
|
|
59
|
-
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
| `library/app/api/toga2.php` | **Unchanged by me; branch merged up** | TRUE-79401 code from `a5e9b844` intact at `:112`, `:8424`, `:8499–8505`. Branch `TRUE-79401` @ `5aca31ea`, clean, `_production` merged in, patch still +17/−2, `php -l` clean. **Not pushed by me.** |
|
|
67
|
+
| `test/@Mark/AIG/test_multi_email.php` | **Created, unstaged** | 12/12 pass on PHP 7.2.33. Mirrors the transform + drift guard + capacity report. |
|
|
68
|
+
| `_underscore/Model/Aig/Entitlement.php` | Read only | `postPost` single-recipient email confirmed correct by design. |
|
|
69
|
+
| `_underscore/Model/Client/ContactEmailAddress.php` | Read only | One row per address; FK `contactId` → `_Model_Client_Contact`. |
|
|
70
|
+
| `C:\xampp_8x\mysql\bin\my.ini` | Read only — **deliberately unchanged** | Diverges from prod (`collation-server`, `sql_mode=''`, `lower_case_table_names=2`); developer chose to leave it. |
|
|
71
|
+
| `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` | **Updated + pushed** (both passes) | Payload shape RESOLVED, open items, corrections, `varchar(255)` ceiling, redundant-UPDATE gotcha, genericized lint path, merge verification. |
|
|
72
|
+
| `knowledge/clients/aig/features/entitlement-intake.md` | **Updated + pushed** (both passes) | Business Unit dropped; collation trap reframed as per-machine. |
|
|
73
|
+
| `knowledge/2.0/apps/api2/features/nested-relationship-writes.md` | **Updated + pushed** (both passes) | 201-with-silent-drop; "addresses vanish" corrected to degradation-to-primary. |
|
|
74
|
+
| `knowledge/2.0/apps/worker2/features/teams-transcript-export.md` | **Updated + pushed** | No transcript coverage before ~2026-06-12. |
|
|
75
|
+
| `knowledge/1.0/apps/test/features/static-no-db-regression-harness.md` | **Created + pushed** | The harness technique, `sqlEscape()` enabler, drift guard. |
|
|
76
|
+
| `knowledge/2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md` | **Created + pushed** | Parity query + the four divergence classes. |
|
|
77
|
+
| `knowledge/2.0/apps/dbchanges2/architecture.md` | **Updated + pushed (⚠ ELEVATED, approved)** | Mandatory staging-column collation pin; per-machine reframing. |
|
|
78
|
+
|
|
79
|
+
**No production source code was written or changed this session** — the only new code is the test harness.
|
|
62
80
|
|
|
63
81
|
## Decisions made
|
|
64
82
|
|
|
65
|
-
- **No api2 intake mapping will be built.**
|
|
66
|
-
- **Keep the separately-escaped `$primaryEmailAddress` for the dedupe lookup**,
|
|
67
|
-
- **Multi-recipient notification stays out of scope
|
|
68
|
-
- **
|
|
69
|
-
-
|
|
83
|
+
- **No api2 intake mapping will be built.** AIG's mapping document targets `Entitlements.contact.contactEmailAddresses[].emailAddress`, so AIG restructures and the V2 metadata engine writes the collection generically. *Rejected:* an AIG `prePost` interceptor converting flat `c_secondaryEmailAddress`/`c_tertiaryEmailAddress` custom fields — caps the address count and needs custom-field definitions plus a release.
|
|
84
|
+
- **Keep the separately-escaped `$primaryEmailAddress` for the dedupe lookup**, against the meeting's "get rid of the primary contact email address." The primary-email `LIKE` is the legacy fallback for 1.0 customers with no `c_togaCustomerId`; removing it breaks that matching outright. Accepted consequence: a stored joined list can't satisfy the wildcard-less `LIKE`.
|
|
85
|
+
- **Multi-recipient notification stays out of scope** — the meeting scoped this to verification/lookup and rejected CC/BCC on TogaDesk tickets.
|
|
86
|
+
- **Test the transform with a mirror + drift guard rather than refactoring production.** A mirror is the only option while the logic is inlined in a DB-writing private method; the guard makes it self-invalidating so it can't silently pass on stale logic. *Rejected for now:* extracting to a `public static` helper (cleaner, but a production change on a ticket that is otherwise code-complete).
|
|
87
|
+
- **The nested array is NOT unbounded** — `varchar(255)` caps `Customers.emailAddress` at roughly 6–7 business-length addresses (2 = 96 chars, 3 = 145, 5 = 243 fit; 8 = 390 and 10 = 489 overflow). The committed scope of 3 addresses (~145 chars) is safe, but don't pitch "send as many as you like" without that caveat.
|
|
88
|
+
- **The `ERROR 1267` collation trap is a per-machine CONFIG property, not prod-only and not a version difference.** Both servers are MySQL 8.0. Mitigation unchanged but better justified: always pin `CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` on staging columns — a local pass is not evidence.
|
|
89
|
+
- **Local MySQL left diverging from prod, deliberately.** Aligning to 2.0 prod would diverge from 1.0 prod, since one server backs both platforms; `lower_case_table_names` can't be changed after datadir init anyway. Consequence: local cannot validate collation, strict-mode truncation, or table-name casing.
|
|
90
|
+
- **`worker2` was NOT added to the `aig` client app-scope** — the transcript-coverage finding is a shared worker2 fact. **`test` was NOT added either** — the harness is a technique artifact in a personal folder.
|
|
91
|
+
- **The collation rule landed in `dbchanges2/architecture.md`** (elevated, approved) rather than only in the workflow doc, so authors trip over it where they work. The one generalization was softened to "tenant tables are *typically* `utf8mb4_unicode_ci` (verified for `Client_Aig.Items.partNumber`)."
|
|
70
92
|
|
|
71
93
|
## Blockers
|
|
72
94
|
|
|
73
|
-
- **Communication deadlock with AIG
|
|
74
|
-
- **Unknown whether the mapping document
|
|
75
|
-
- **Deploy state of the library change is unverified** —
|
|
95
|
+
- **Communication deadlock with AIG — not a code blocker.** AIG will not enable the secondary/tertiary email fields until we tell them the feature is live; our 2026-05-04 email asked them to send the fields "only if they are necessary for the process" — ambiguous phrasing delivered in the same breath as dropping Business Unit, which may have read as a de-scope. **No AIG reply to that message.** Both sides are waiting on each other.
|
|
96
|
+
- **Unknown whether the mapping document ever reached AIG.** Paulina was holding it until the Business Unit question was answered; that was answered 2026-05-04, but nothing confirms the document went out afterward.
|
|
97
|
+
- **Deploy state of the library change is unverified** — can't honestly tell AIG "it's live" until confirmed.
|
|
98
|
+
- **`origin/_production` freshness unconfirmed** — local matches origin as of the last fetch, but no `git fetch` was run this session.
|
|
76
99
|
|
|
77
100
|
## Exact next step
|
|
78
101
|
|
|
79
|
-
> Draft the note to Paulina for AIG's dev team. It must (1) retract the "only if necessary" ambiguity and state plainly that the secondary/tertiary addresses **are** wanted — unlike Business Unit; (2) include the concrete JSON example, not just the `contactEmailAddresses[].emailAddress` path expression, because a bare-string array
|
|
102
|
+
> Draft the note to Paulina for AIG's dev team. It must (1) retract the "only if necessary" ambiguity and state plainly that the secondary/tertiary addresses **are** wanted — unlike Business Unit; (2) include the concrete JSON example, not just the `contactEmailAddresses[].emailAddress` path expression, because a bare-string array returns HTTP 201 and stores **only the primary** — indistinguishable from the feature being off; (3) ask the two confirmations — did the mapping document reach AIG after the 2026-05-04 email, and has their dev acknowledged the `contactEmailAddresses[]` mapping. Mention that 3 addresses is safe but the 1.0 column caps out around 6–7. The payload fragment to paste is in `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` → the `TRUE-79401 payload shape — RESOLVED` section. Immediately after: check the git/deploy state of `C:\WWW\library` for the TRUE-79401 commit before signalling "live".
|
|
80
103
|
|
|
81
104
|
---
|
|
82
105
|
_Saved by /session-save on 2026-08-03_
|
package/package.json
CHANGED